jd-intel-mcp 0.1.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 +119 -0
- package/cli.js +37 -0
- package/descriptions.js +83 -0
- package/envelope.js +38 -0
- package/errors.js +16 -0
- package/install.js +142 -0
- package/package.json +58 -0
- package/resources.js +40 -0
- package/server.js +46 -0
- package/tools.js +133 -0
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# jd-intel-mcp
|
|
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 Ashby through natural conversation.
|
|
4
|
+
|
|
5
|
+
> **Stop pasting job descriptions into AI assistants. Let your AI fetch them directly.**
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What you can ask
|
|
10
|
+
|
|
11
|
+
- "Is Stripe hiring PMs in the US?"
|
|
12
|
+
- "Find remote engineering roles at fintech companies, posted in the last two weeks, then draft a cover letter for the best match."
|
|
13
|
+
- "What companies in your index are in the developer tools space?"
|
|
14
|
+
- "Does Figma use Greenhouse or Lever?"
|
|
15
|
+
|
|
16
|
+
The AI handles the phrasing. The MCP server handles the calls, filters, and normalizes results. No copy-paste.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Install (Claude Desktop)
|
|
21
|
+
|
|
22
|
+
Add this to your Claude Desktop config file:
|
|
23
|
+
|
|
24
|
+
**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
25
|
+
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"mcpServers": {
|
|
30
|
+
"jd-intel": {
|
|
31
|
+
"command": "npx",
|
|
32
|
+
"args": ["-y", "jd-intel-mcp"]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Restart Claude Desktop. The tools appear automatically.
|
|
39
|
+
|
|
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
|
+
---
|
|
48
|
+
|
|
49
|
+
## Tools exposed
|
|
50
|
+
|
|
51
|
+
| Tool | Purpose |
|
|
52
|
+
|------|---------|
|
|
53
|
+
| `fetch_jobs` | Get open roles at a company, with filters for role type, topic, location, and recency |
|
|
54
|
+
| `search_registry` | Find companies by name or sector |
|
|
55
|
+
| `detect_ats` | Identify which ATS platform a company uses |
|
|
56
|
+
|
|
57
|
+
Plus one Resource: `registry://jd-intel/all`. Full company registry, grouped by ATS, for broad catalog surveys.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Filter design
|
|
62
|
+
|
|
63
|
+
See the main library [docs/filters.md](../docs/filters.md) for the full rationale. Short version:
|
|
64
|
+
|
|
65
|
+
- Use `title_filter` for role identity ("product manager", "staff engineer"). Matches title only.
|
|
66
|
+
- Use `filter` for topic or scope ("integrations", "growth"). Matches across title, department, description.
|
|
67
|
+
- They AND together. Use both for "PM roles about integrations".
|
|
68
|
+
- For US queries: `location_includes: ["United States", "US", "Remote - US"]`. Avoid bare "Remote" (matches Remote-EMEA etc.).
|
|
69
|
+
- Short codes like "US", "UK" are safe. They use word-boundary matching to prevent collisions with "Australia", "Auckland", etc.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Local development
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
cd mcp
|
|
77
|
+
npm install
|
|
78
|
+
node server.js
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The server prints `jd-intel MCP server running on stdio` and then listens on stdin/stdout. For quick testing, point Claude Desktop at the local path:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"mcpServers": {
|
|
86
|
+
"jd-intel-dev": {
|
|
87
|
+
"command": "node",
|
|
88
|
+
"args": ["/absolute/path/to/jd-intel/mcp/server.js"]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Response shape
|
|
97
|
+
|
|
98
|
+
All three tools return a uniform envelope:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"status": "success" | "partial" | "error",
|
|
103
|
+
"data": <tool-specific>,
|
|
104
|
+
"metadata": {
|
|
105
|
+
"attempted": [...],
|
|
106
|
+
"succeeded": [...],
|
|
107
|
+
"failed": {...},
|
|
108
|
+
"notes": [...]
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
On errors, the envelope adds `"error": { "code", "message" }`. Error codes come from a fixed taxonomy (`company_not_found`, `ats_unreachable`, `invalid_args`, `partial_failure`, `rate_limited`, `no_results`).
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT
|
package/cli.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dispatcher entry point.
|
|
5
|
+
*
|
|
6
|
+
* - No args (default, how Claude Desktop invokes us) → start the MCP server
|
|
7
|
+
* - "install" → set up Claude Desktop config
|
|
8
|
+
* - "uninstall" → remove from Claude Desktop config
|
|
9
|
+
* - "help" / "-h" / "--help" → show usage
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const [, , command] = process.argv;
|
|
13
|
+
|
|
14
|
+
switch (command) {
|
|
15
|
+
case 'install': {
|
|
16
|
+
const { install } = await import('./install.js');
|
|
17
|
+
await install();
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
case 'uninstall': {
|
|
21
|
+
const { uninstall } = await import('./install.js');
|
|
22
|
+
await uninstall();
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
case 'help':
|
|
26
|
+
case '-h':
|
|
27
|
+
case '--help': {
|
|
28
|
+
const { printHelp } = await import('./install.js');
|
|
29
|
+
printHelp();
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
default: {
|
|
33
|
+
// No command → start the MCP server.
|
|
34
|
+
// This is the path Claude Desktop invokes via `npx -y jd-intel-mcp`.
|
|
35
|
+
await import('./server.js');
|
|
36
|
+
}
|
|
37
|
+
}
|
package/descriptions.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool descriptions — the semantic contract each tool exposes to the AI.
|
|
3
|
+
*
|
|
4
|
+
* These strings are loaded into the AI's context on every turn. Every
|
|
5
|
+
* sentence must earn its place. Dense > long. Target: 200-400 tokens each.
|
|
6
|
+
*
|
|
7
|
+
* Updating these strings is a product decision, not a docs task —
|
|
8
|
+
* the AI's behavior changes immediately when descriptions change.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const FETCH_JOBS = `Fetch open job postings from a specific company's ATS (Greenhouse, Lever, or Ashby).
|
|
12
|
+
|
|
13
|
+
USE WHEN: the user asks about roles at a known company ("Is Stripe hiring?", "What's open at Figma?").
|
|
14
|
+
|
|
15
|
+
DON'T USE WHEN:
|
|
16
|
+
- User doesn't know the company → read the registry Resource or call search_registry
|
|
17
|
+
- User only asks which ATS a company uses → call detect_ats
|
|
18
|
+
|
|
19
|
+
ARGUMENT GUIDE:
|
|
20
|
+
|
|
21
|
+
company: lowercase slug, no spaces (e.g. "stripe", "cockroachlabs"). Hyphens and spaces auto-stripped.
|
|
22
|
+
|
|
23
|
+
title_filter: JavaScript-compatible regex matched against TITLE ONLY. Case-insensitive by default. Do NOT use inline flags like (?i) (not supported by V8). Use for role identity ("product manager", "staff engineer"). Does NOT match description text. That's the distinction from filter.
|
|
24
|
+
|
|
25
|
+
filter: JavaScript-compatible regex matched across title + department + description. Case-insensitive by default. Do NOT use inline flags like (?i). Use for topic/scope ("integrations", "growth"). AND'd with title_filter.
|
|
26
|
+
|
|
27
|
+
posted_within_days: number. "recent" or "new" → 30. "this week" → 7. "today" → 1.
|
|
28
|
+
|
|
29
|
+
location_includes: array of keywords. Case-insensitive substring match; short codes (US, UK) use word-boundary matching automatically. For US queries prefer ["United States", "US", "Remote - US"]. Avoid bare "Remote". It matches Remote-EMEA, Remote-LatAm.
|
|
30
|
+
|
|
31
|
+
location_excludes: array. Drop jobs whose location contains any keyword. Use as refinement on top of includes.
|
|
32
|
+
|
|
33
|
+
limit: default 100. Reduce for high-volume companies.
|
|
34
|
+
|
|
35
|
+
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
|
+
|
|
37
|
+
ERROR CODES:
|
|
38
|
+
- company_not_found: slug not in registry, not detected
|
|
39
|
+
- ats_unreachable: known ATS failed
|
|
40
|
+
- invalid_args: missing/malformed args
|
|
41
|
+
- rate_limited: upstream 429`;
|
|
42
|
+
|
|
43
|
+
export const SEARCH_REGISTRY = `Find companies in the indexed registry by name or sector.
|
|
44
|
+
|
|
45
|
+
USE WHEN: targeted lookups ("Is Stripe in your index?", "Show me fintech companies").
|
|
46
|
+
|
|
47
|
+
DON'T USE WHEN:
|
|
48
|
+
- User wants a broad survey of the catalog → read the registry://jd-intel/all Resource instead (one fetch vs repeated tool calls)
|
|
49
|
+
- User asks about a specific company's jobs → call fetch_jobs directly
|
|
50
|
+
|
|
51
|
+
ARGUMENT GUIDE:
|
|
52
|
+
|
|
53
|
+
query: optional. Substring match (case-insensitive) against company name.
|
|
54
|
+
sector: optional. Match against sector field. Examples: "fintech", "developer tools", "marketing tech".
|
|
55
|
+
|
|
56
|
+
At least one argument required. Returns companies matching either.
|
|
57
|
+
|
|
58
|
+
RESPONSE: { status, data: [{ slug, name, sector, ats }], metadata }. Each result includes the ATS platform for that company.
|
|
59
|
+
|
|
60
|
+
ERROR CODES:
|
|
61
|
+
- invalid_args: both query and sector missing`;
|
|
62
|
+
|
|
63
|
+
export const DETECT_ATS = `Detect which ATS platform (Greenhouse, Lever, or Ashby) a company uses.
|
|
64
|
+
|
|
65
|
+
USE WHEN: user asks about the ATS platform explicitly ("What ATS does Stripe use?") or for debugging.
|
|
66
|
+
|
|
67
|
+
DON'T USE WHEN: you want to fetch jobs. Call fetch_jobs directly (it auto-detects internally).
|
|
68
|
+
|
|
69
|
+
ARGUMENT GUIDE:
|
|
70
|
+
|
|
71
|
+
company: company name or slug. Hyphens and spaces stripped automatically ("Cockroach Labs" → "cockroachlabs").
|
|
72
|
+
|
|
73
|
+
RESPONSE: { status, data: "greenhouse" | "lever" | "ashby" | null, metadata }. data === null means no supported ATS hosts this company. "partial" status means some probes failed. Result may be incomplete.
|
|
74
|
+
|
|
75
|
+
ERROR CODES:
|
|
76
|
+
- invalid_args: company arg missing
|
|
77
|
+
- partial_failure: some probes failed`;
|
|
78
|
+
|
|
79
|
+
export const REGISTRY_RESOURCE = `The full jd-intel company registry, grouped by ATS platform.
|
|
80
|
+
|
|
81
|
+
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
|
+
|
|
83
|
+
Shape: { greenhouse: [{slug, name, sector}], lever: [...], ashby: [...] }.`;
|
package/envelope.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uniform response envelope for all MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* Shape: { status, data, metadata }
|
|
5
|
+
* status: "success" | "partial" | "error"
|
|
6
|
+
*
|
|
7
|
+
* The envelope is serialized as JSON text inside an MCP content block.
|
|
8
|
+
* Every tool uses this so the AI learns one response pattern that works
|
|
9
|
+
* across success, partial-failure, and error paths.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export function success(data, metadata = {}) {
|
|
13
|
+
return wrap({ status: 'success', data, metadata });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function partial(data, metadata = {}) {
|
|
17
|
+
return wrap({ status: 'partial', data, metadata });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function error(code, message, metadata = {}) {
|
|
21
|
+
return wrap({
|
|
22
|
+
status: 'error',
|
|
23
|
+
data: null,
|
|
24
|
+
error: { code, message },
|
|
25
|
+
metadata,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function wrap(payload) {
|
|
30
|
+
return {
|
|
31
|
+
content: [
|
|
32
|
+
{
|
|
33
|
+
type: 'text',
|
|
34
|
+
text: JSON.stringify(payload, null, 2),
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
}
|
package/errors.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error code taxonomy for all MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* Codes are short and stable. Messages are short and factual.
|
|
5
|
+
* The tool description teaches the AI what each code means; errors
|
|
6
|
+
* themselves do not need to be verbose.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const ERROR_CODES = {
|
|
10
|
+
COMPANY_NOT_FOUND: 'company_not_found', // Slug not in registry and not detected
|
|
11
|
+
ATS_UNREACHABLE: 'ats_unreachable', // Known ATS failed (500, timeout)
|
|
12
|
+
PARTIAL_FAILURE: 'partial_failure', // Discovery mode; some adapters failed
|
|
13
|
+
INVALID_ARGS: 'invalid_args', // Missing required, wrong type, bad pattern
|
|
14
|
+
NO_RESULTS: 'no_results', // Query succeeded, filters returned nothing
|
|
15
|
+
RATE_LIMITED: 'rate_limited', // Upstream returned 429
|
|
16
|
+
};
|
package/install.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-command installer for jd-intel-mcp in Claude Desktop.
|
|
3
|
+
*
|
|
4
|
+
* Usage: npx jd-intel-mcp install
|
|
5
|
+
*
|
|
6
|
+
* What it does:
|
|
7
|
+
* 1. Detects the user's OS
|
|
8
|
+
* 2. Locates Claude Desktop's config file
|
|
9
|
+
* 3. Reads existing config (preserves other MCP servers)
|
|
10
|
+
* 4. Adds or updates the jd-intel entry
|
|
11
|
+
* 5. Writes back as valid JSON
|
|
12
|
+
* 6. Prints next steps
|
|
13
|
+
*
|
|
14
|
+
* Prevents the "paste a snippet into existing config and break JSON" error
|
|
15
|
+
* that hand-editing reliably produces.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
19
|
+
import { existsSync } from 'node:fs';
|
|
20
|
+
import { dirname, join } from 'node:path';
|
|
21
|
+
import { homedir, platform } from 'node:os';
|
|
22
|
+
|
|
23
|
+
const PACKAGE_NAME = 'jd-intel-mcp';
|
|
24
|
+
const SERVER_KEY = 'jd-intel';
|
|
25
|
+
|
|
26
|
+
function configPathFor(os) {
|
|
27
|
+
const home = homedir();
|
|
28
|
+
switch (os) {
|
|
29
|
+
case 'darwin':
|
|
30
|
+
return join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
31
|
+
case 'win32':
|
|
32
|
+
return join(process.env.APPDATA || join(home, 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
|
|
33
|
+
case 'linux':
|
|
34
|
+
return join(home, '.config', 'Claude', 'claude_desktop_config.json');
|
|
35
|
+
default:
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function install() {
|
|
41
|
+
const os = platform();
|
|
42
|
+
const configPath = configPathFor(os);
|
|
43
|
+
|
|
44
|
+
if (!configPath) {
|
|
45
|
+
console.error(`Unsupported platform: ${os}. Supported: macOS, Windows, Linux.`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(`Installing ${PACKAGE_NAME} in Claude Desktop config.`);
|
|
50
|
+
console.log(`Config file: ${configPath}\n`);
|
|
51
|
+
|
|
52
|
+
let config = {};
|
|
53
|
+
let existed = false;
|
|
54
|
+
|
|
55
|
+
if (existsSync(configPath)) {
|
|
56
|
+
existed = true;
|
|
57
|
+
const raw = await readFile(configPath, 'utf-8');
|
|
58
|
+
try {
|
|
59
|
+
config = JSON.parse(raw);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error('Your existing Claude Desktop config is not valid JSON.');
|
|
62
|
+
console.error(`Fix it manually first, or back it up and run this again.`);
|
|
63
|
+
console.error(`Error: ${err.message}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
// Make sure the parent directory exists before writing
|
|
68
|
+
await mkdir(dirname(configPath), { recursive: true });
|
|
69
|
+
console.log('Config file did not exist — creating it.\n');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!config.mcpServers || typeof config.mcpServers !== 'object') {
|
|
73
|
+
config.mcpServers = {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const alreadyInstalled = Boolean(config.mcpServers[SERVER_KEY]);
|
|
77
|
+
|
|
78
|
+
config.mcpServers[SERVER_KEY] = {
|
|
79
|
+
command: 'npx',
|
|
80
|
+
args: ['-y', PACKAGE_NAME],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
84
|
+
|
|
85
|
+
console.log(
|
|
86
|
+
alreadyInstalled
|
|
87
|
+
? `Updated existing ${SERVER_KEY} entry.`
|
|
88
|
+
: `Added ${SERVER_KEY} to mcpServers.`
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const otherServers = Object.keys(config.mcpServers).filter((k) => k !== SERVER_KEY);
|
|
92
|
+
if (otherServers.length > 0) {
|
|
93
|
+
console.log(`Preserved other MCP servers: ${otherServers.join(', ')}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log('\nNext steps:');
|
|
97
|
+
console.log(' 1. Fully quit Claude Desktop (system tray → Quit on Windows, ⌘Q on macOS)');
|
|
98
|
+
console.log(' 2. Reopen Claude Desktop');
|
|
99
|
+
console.log(' 3. Start a new chat — jd-intel tools will be available');
|
|
100
|
+
console.log('\nTry: "What fintech companies are in your jd-intel?"\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function uninstall() {
|
|
104
|
+
const os = platform();
|
|
105
|
+
const configPath = configPathFor(os);
|
|
106
|
+
|
|
107
|
+
if (!configPath || !existsSync(configPath)) {
|
|
108
|
+
console.log('No Claude Desktop config found. Nothing to uninstall.');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const raw = await readFile(configPath, 'utf-8');
|
|
113
|
+
let config;
|
|
114
|
+
try {
|
|
115
|
+
config = JSON.parse(raw);
|
|
116
|
+
} catch {
|
|
117
|
+
console.error('Existing config is not valid JSON. Leaving it alone.');
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!config.mcpServers || !config.mcpServers[SERVER_KEY]) {
|
|
122
|
+
console.log(`${SERVER_KEY} is not installed. Nothing to do.`);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
delete config.mcpServers[SERVER_KEY];
|
|
127
|
+
await writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
128
|
+
|
|
129
|
+
console.log(`Removed ${SERVER_KEY} from Claude Desktop config.`);
|
|
130
|
+
console.log('Restart Claude Desktop to complete the uninstall.');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function printHelp() {
|
|
134
|
+
console.log(`jd-intel-mcp — MCP server for searching jobs across ATS platforms
|
|
135
|
+
|
|
136
|
+
Usage:
|
|
137
|
+
npx jd-intel-mcp Start the MCP server (invoked by Claude Desktop)
|
|
138
|
+
npx jd-intel-mcp install Configure Claude Desktop to use this server
|
|
139
|
+
npx jd-intel-mcp uninstall Remove this server from Claude Desktop config
|
|
140
|
+
npx jd-intel-mcp help Show this help message
|
|
141
|
+
`);
|
|
142
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jd-intel-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for jd-intel. Your AI assistant fetches and reasons over full job descriptions, no copy-paste.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "server.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"jd-intel-mcp": "./cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"cli.js",
|
|
12
|
+
"server.js",
|
|
13
|
+
"tools.js",
|
|
14
|
+
"resources.js",
|
|
15
|
+
"envelope.js",
|
|
16
|
+
"errors.js",
|
|
17
|
+
"descriptions.js",
|
|
18
|
+
"install.js",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"start": "node server.js",
|
|
23
|
+
"dev": "node --watch server.js",
|
|
24
|
+
"install:local": "node cli.js install",
|
|
25
|
+
"uninstall:local": "node cli.js uninstall"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
29
|
+
"jd-intel": "^0.1.0",
|
|
30
|
+
"zod": "^3.23.0"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"mcp",
|
|
34
|
+
"model-context-protocol",
|
|
35
|
+
"jd",
|
|
36
|
+
"job-description",
|
|
37
|
+
"jobs",
|
|
38
|
+
"ats",
|
|
39
|
+
"claude",
|
|
40
|
+
"greenhouse",
|
|
41
|
+
"lever",
|
|
42
|
+
"ashby"
|
|
43
|
+
],
|
|
44
|
+
"author": "Prashant R",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/prPMDev/jd-intel.git",
|
|
49
|
+
"directory": "mcp"
|
|
50
|
+
},
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/prPMDev/jd-intel/issues"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/prPMDev/jd-intel#readme",
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=18"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/resources.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register Resources on the MCP server.
|
|
3
|
+
*
|
|
4
|
+
* The registry is exposed as a Resource for broad catalog surveys.
|
|
5
|
+
* Tools (search_registry) handle directed queries; the Resource
|
|
6
|
+
* covers the "tell me about the whole index" intent without needing
|
|
7
|
+
* repeated tool calls.
|
|
8
|
+
*
|
|
9
|
+
* Resources are lazy-loaded — the AI only fetches when it decides
|
|
10
|
+
* it needs the full catalog. One fetch per session for broad reasoning.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { registry } from 'jd-intel';
|
|
14
|
+
|
|
15
|
+
const { load: loadRegistry } = registry;
|
|
16
|
+
import { REGISTRY_RESOURCE } from './descriptions.js';
|
|
17
|
+
|
|
18
|
+
export function registerResources(server) {
|
|
19
|
+
server.registerResource(
|
|
20
|
+
'registry',
|
|
21
|
+
'registry://jd-intel/all',
|
|
22
|
+
{
|
|
23
|
+
title: 'jd-intel company registry',
|
|
24
|
+
description: REGISTRY_RESOURCE,
|
|
25
|
+
mimeType: 'application/json',
|
|
26
|
+
},
|
|
27
|
+
async (uri) => {
|
|
28
|
+
const all = await loadRegistry();
|
|
29
|
+
return {
|
|
30
|
+
contents: [
|
|
31
|
+
{
|
|
32
|
+
uri: uri.href,
|
|
33
|
+
mimeType: 'application/json',
|
|
34
|
+
text: JSON.stringify(all, null, 2),
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* jd-intel MCP server — entry point.
|
|
5
|
+
*
|
|
6
|
+
* Exposes fetch_jobs, search_registry, and detect_ats as MCP tools,
|
|
7
|
+
* plus the full company registry as a Resource, over stdio transport.
|
|
8
|
+
*
|
|
9
|
+
* Run locally: node mcp/server.js
|
|
10
|
+
* Via Claude Desktop config:
|
|
11
|
+
* {
|
|
12
|
+
* "mcpServers": {
|
|
13
|
+
* "jd-intel": {
|
|
14
|
+
* "command": "npx",
|
|
15
|
+
* "args": ["-y", "jd-intel-mcp"]
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
22
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
23
|
+
|
|
24
|
+
import { registerTools } from './tools.js';
|
|
25
|
+
import { registerResources } from './resources.js';
|
|
26
|
+
|
|
27
|
+
async function main() {
|
|
28
|
+
const server = new McpServer({
|
|
29
|
+
name: 'jd-intel',
|
|
30
|
+
version: '0.1.0',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
registerTools(server);
|
|
34
|
+
registerResources(server);
|
|
35
|
+
|
|
36
|
+
const transport = new StdioServerTransport();
|
|
37
|
+
await server.connect(transport);
|
|
38
|
+
|
|
39
|
+
// Log to stderr so stdout stays clean for MCP protocol traffic
|
|
40
|
+
console.error('jd-intel MCP server running on stdio');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
main().catch((err) => {
|
|
44
|
+
console.error('Fatal error starting jd-intel MCP:', err);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|
package/tools.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register all three tools on the MCP server.
|
|
3
|
+
*
|
|
4
|
+
* Each handler:
|
|
5
|
+
* 1. Validates args (Zod handles most of this)
|
|
6
|
+
* 2. Calls the jd-intel library
|
|
7
|
+
* 3. Wraps the result in the uniform envelope
|
|
8
|
+
*
|
|
9
|
+
* Handlers stay thin — library does the work, MCP layer shapes the response.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { fetchJobs, detectAts as libDetectAts, registry } from 'jd-intel';
|
|
14
|
+
|
|
15
|
+
const { search: searchRegistry, findAtsBySlug } = registry;
|
|
16
|
+
import { success, partial, error } from './envelope.js';
|
|
17
|
+
import { ERROR_CODES } from './errors.js';
|
|
18
|
+
import {
|
|
19
|
+
FETCH_JOBS,
|
|
20
|
+
SEARCH_REGISTRY,
|
|
21
|
+
DETECT_ATS,
|
|
22
|
+
} from './descriptions.js';
|
|
23
|
+
|
|
24
|
+
export function registerTools(server) {
|
|
25
|
+
server.registerTool(
|
|
26
|
+
'fetch_jobs',
|
|
27
|
+
{
|
|
28
|
+
title: 'Fetch jobs from a company ATS',
|
|
29
|
+
description: FETCH_JOBS,
|
|
30
|
+
inputSchema: {
|
|
31
|
+
company: z.string().describe('Company slug or name (e.g. "stripe")'),
|
|
32
|
+
title_filter: z.string().optional().describe('Regex matched against title only — role identity'),
|
|
33
|
+
filter: z.string().optional().describe('Regex matched across title, department, description — topic/scope'),
|
|
34
|
+
posted_within_days: z.number().int().positive().optional().describe('Only jobs posted within N days'),
|
|
35
|
+
location_includes: z.array(z.string()).optional().describe('Keep jobs whose location contains any keyword'),
|
|
36
|
+
location_excludes: z.array(z.string()).optional().describe('Drop jobs whose location contains any keyword'),
|
|
37
|
+
limit: z.number().int().positive().optional().describe('Cap results (default 100)'),
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
async (args) => {
|
|
41
|
+
try {
|
|
42
|
+
const jobs = await fetchJobs({
|
|
43
|
+
company: args.company,
|
|
44
|
+
titleFilter: args.title_filter,
|
|
45
|
+
filter: args.filter,
|
|
46
|
+
postedWithinDays: args.posted_within_days,
|
|
47
|
+
locationIncludes: args.location_includes,
|
|
48
|
+
locationExcludes: args.location_excludes,
|
|
49
|
+
limit: args.limit,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const normalizedSlug = args.company.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
53
|
+
const registryAts = await findAtsBySlug(normalizedSlug);
|
|
54
|
+
|
|
55
|
+
return success(jobs, {
|
|
56
|
+
count: jobs.length,
|
|
57
|
+
registry_hit: registryAts !== null,
|
|
58
|
+
ats: registryAts,
|
|
59
|
+
});
|
|
60
|
+
} catch (err) {
|
|
61
|
+
return error(ERROR_CODES.INVALID_ARGS, err.message || 'Unknown error');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
server.registerTool(
|
|
67
|
+
'search_registry',
|
|
68
|
+
{
|
|
69
|
+
title: 'Search the company registry',
|
|
70
|
+
description: SEARCH_REGISTRY,
|
|
71
|
+
inputSchema: {
|
|
72
|
+
query: z.string().optional().describe('Substring match against company name'),
|
|
73
|
+
sector: z.string().optional().describe('Match against sector (e.g. "fintech", "developer tools")'),
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
async (args) => {
|
|
77
|
+
if (!args.query && !args.sector) {
|
|
78
|
+
return error(ERROR_CODES.INVALID_ARGS, 'Provide query or sector');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// searchRegistry searches both name and sector via a single query string.
|
|
82
|
+
// We combine args into a single search string, preferring query if both given.
|
|
83
|
+
const searchTerm = args.query || args.sector;
|
|
84
|
+
const results = await searchRegistry(searchTerm);
|
|
85
|
+
|
|
86
|
+
// If sector was specified, further filter by sector match
|
|
87
|
+
const filtered = args.sector
|
|
88
|
+
? results.filter((r) => (r.sector || '').toLowerCase().includes(args.sector.toLowerCase()))
|
|
89
|
+
: results;
|
|
90
|
+
|
|
91
|
+
return success(filtered, {
|
|
92
|
+
count: filtered.length,
|
|
93
|
+
query: args.query || null,
|
|
94
|
+
sector: args.sector || null,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
server.registerTool(
|
|
100
|
+
'detect_ats',
|
|
101
|
+
{
|
|
102
|
+
title: 'Detect which ATS a company uses',
|
|
103
|
+
description: DETECT_ATS,
|
|
104
|
+
inputSchema: {
|
|
105
|
+
company: z.string().describe('Company name or slug'),
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
async (args) => {
|
|
109
|
+
const results = await libDetectAts(args.company);
|
|
110
|
+
|
|
111
|
+
if (results.length === 0) {
|
|
112
|
+
return success(null, { attempted: ['greenhouse', 'lever', 'ashby'], succeeded: [] });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (results.length === 1) {
|
|
116
|
+
return success(results[0].ats, {
|
|
117
|
+
attempted: ['greenhouse', 'lever', 'ashby'],
|
|
118
|
+
succeeded: [results[0].ats],
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Multiple matches — rare but possible if a company is registered on more than one ATS
|
|
123
|
+
return partial(
|
|
124
|
+
results[0].ats,
|
|
125
|
+
{
|
|
126
|
+
attempted: ['greenhouse', 'lever', 'ashby'],
|
|
127
|
+
succeeded: results.map((r) => r.ats),
|
|
128
|
+
notes: [`Company found on multiple platforms: ${results.map((r) => r.ats).join(', ')}. Returning first match.`],
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
}
|