jobseek-mcp 0.2.0 → 0.4.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 +12 -0
- package/dist/index.js +8 -5
- package/dist/resources.d.ts +25 -0
- package/dist/resources.js +122 -0
- package/dist/tools/launch-pad.d.ts +20 -0
- package/dist/tools/launch-pad.js +109 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,6 +51,18 @@ After saving the config, restart Claude Code. Type `/mcp` to verify the server i
|
|
|
51
51
|
|
|
52
52
|
## Available Tools
|
|
53
53
|
|
|
54
|
+
### `upload_resume`
|
|
55
|
+
Upload a PDF resume to JobSeek.
|
|
56
|
+
|
|
57
|
+
**Example prompts:**
|
|
58
|
+
- "Upload my resume from ~/Documents/resume.pdf"
|
|
59
|
+
- "Upload /Users/me/Downloads/Resume_2025.pdf to JobSeek"
|
|
60
|
+
|
|
61
|
+
**What it does:**
|
|
62
|
+
- Reads PDF from your local filesystem
|
|
63
|
+
- Extracts text and parses with AI
|
|
64
|
+
- Stores the parsed resume in JobSeek
|
|
65
|
+
|
|
54
66
|
### `optimize_resume`
|
|
55
67
|
Optimize your resume for ATS (Applicant Tracking Systems).
|
|
56
68
|
|
package/dist/index.js
CHANGED
|
@@ -5,13 +5,15 @@ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSche
|
|
|
5
5
|
import { optimizeResumeTool, handleOptimizeResume } from "./tools/optimize-resume.js";
|
|
6
6
|
import { downloadResumePdfTool, handleDownloadResumePdf } from "./tools/download-resume-pdf.js";
|
|
7
7
|
import { uploadResumeTool, handleUploadResume } from "./tools/upload-resume.js";
|
|
8
|
+
import { launchPadTool, handleLaunchPad } from "./tools/launch-pad.js";
|
|
9
|
+
import { allResources, handleReadResource } from "./resources.js";
|
|
8
10
|
// Configuration
|
|
9
11
|
const JOBSEEK_API_URL = process.env.JOBSEEK_API_URL || "https://jobseek-iota.vercel.app";
|
|
10
12
|
const JOBSEEK_API_KEY = process.env.JOBSEEK_API_KEY;
|
|
11
13
|
// Create the MCP server
|
|
12
14
|
const server = new Server({
|
|
13
15
|
name: "jobseek-mcp",
|
|
14
|
-
version: "0.
|
|
16
|
+
version: "0.4.0",
|
|
15
17
|
}, {
|
|
16
18
|
capabilities: {
|
|
17
19
|
tools: {},
|
|
@@ -25,6 +27,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
25
27
|
optimizeResumeTool,
|
|
26
28
|
downloadResumePdfTool,
|
|
27
29
|
uploadResumeTool,
|
|
30
|
+
launchPadTool,
|
|
28
31
|
],
|
|
29
32
|
};
|
|
30
33
|
});
|
|
@@ -38,6 +41,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
38
41
|
return handleDownloadResumePdf(args, JOBSEEK_API_URL, JOBSEEK_API_KEY);
|
|
39
42
|
case "upload_resume":
|
|
40
43
|
return handleUploadResume(args, JOBSEEK_API_URL, JOBSEEK_API_KEY);
|
|
44
|
+
case "launch_pad":
|
|
45
|
+
return handleLaunchPad(args, JOBSEEK_API_URL, JOBSEEK_API_KEY);
|
|
41
46
|
default:
|
|
42
47
|
throw new Error(`Unknown tool: ${name}`);
|
|
43
48
|
}
|
|
@@ -45,15 +50,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
45
50
|
// List available resources
|
|
46
51
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
47
52
|
return {
|
|
48
|
-
resources:
|
|
49
|
-
// Resources will be added here (resume, applications, etc.)
|
|
50
|
-
],
|
|
53
|
+
resources: allResources,
|
|
51
54
|
};
|
|
52
55
|
});
|
|
53
56
|
// Handle resource reads
|
|
54
57
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
55
58
|
const { uri } = request.params;
|
|
56
|
-
|
|
59
|
+
return handleReadResource(uri);
|
|
57
60
|
});
|
|
58
61
|
// Start the server
|
|
59
62
|
async function main() {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare const resumeResource: {
|
|
2
|
+
uri: string;
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
mimeType: string;
|
|
6
|
+
};
|
|
7
|
+
export declare const applicationsResource: {
|
|
8
|
+
uri: string;
|
|
9
|
+
name: string;
|
|
10
|
+
description: string;
|
|
11
|
+
mimeType: string;
|
|
12
|
+
};
|
|
13
|
+
export declare const allResources: {
|
|
14
|
+
uri: string;
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
mimeType: string;
|
|
18
|
+
}[];
|
|
19
|
+
export declare function handleReadResource(uri: string): Promise<{
|
|
20
|
+
contents: Array<{
|
|
21
|
+
uri: string;
|
|
22
|
+
mimeType: string;
|
|
23
|
+
text: string;
|
|
24
|
+
}>;
|
|
25
|
+
}>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Resource definitions and handlers for MCP
|
|
2
|
+
// Configuration
|
|
3
|
+
const getApiUrl = () => process.env.JOBSEEK_API_URL || "https://getjobseek.com";
|
|
4
|
+
const getApiKey = () => process.env.JOBSEEK_API_KEY;
|
|
5
|
+
// Resource definitions
|
|
6
|
+
export const resumeResource = {
|
|
7
|
+
uri: "jobseek://resume",
|
|
8
|
+
name: "Current Resume",
|
|
9
|
+
description: "Your resume data stored in JobSeek including skills, experience, education, and summary",
|
|
10
|
+
mimeType: "application/json"
|
|
11
|
+
};
|
|
12
|
+
export const applicationsResource = {
|
|
13
|
+
uri: "jobseek://applications",
|
|
14
|
+
name: "Job Applications",
|
|
15
|
+
description: "Your tracked job applications with status, company, and match scores",
|
|
16
|
+
mimeType: "application/json"
|
|
17
|
+
};
|
|
18
|
+
// All resources
|
|
19
|
+
export const allResources = [
|
|
20
|
+
resumeResource,
|
|
21
|
+
applicationsResource,
|
|
22
|
+
];
|
|
23
|
+
// Fetch resume data
|
|
24
|
+
async function fetchResume() {
|
|
25
|
+
const apiKey = getApiKey();
|
|
26
|
+
const apiUrl = getApiUrl();
|
|
27
|
+
if (!apiKey) {
|
|
28
|
+
return JSON.stringify({
|
|
29
|
+
error: "No API key configured",
|
|
30
|
+
help: "Get your API key at " + apiUrl + "/dashboard/api-keys"
|
|
31
|
+
}, null, 2);
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const response = await fetch(`${apiUrl}/api/resumes/current`, {
|
|
35
|
+
headers: {
|
|
36
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
if (response.status === 401) {
|
|
41
|
+
return JSON.stringify({
|
|
42
|
+
error: "Invalid API key",
|
|
43
|
+
help: "Generate a new key at " + apiUrl + "/dashboard/api-keys"
|
|
44
|
+
}, null, 2);
|
|
45
|
+
}
|
|
46
|
+
if (response.status === 404) {
|
|
47
|
+
return JSON.stringify({
|
|
48
|
+
error: "No resume found",
|
|
49
|
+
help: "Upload a resume first using the upload_resume tool"
|
|
50
|
+
}, null, 2);
|
|
51
|
+
}
|
|
52
|
+
return JSON.stringify({
|
|
53
|
+
error: `Failed to fetch resume: ${response.status}`
|
|
54
|
+
}, null, 2);
|
|
55
|
+
}
|
|
56
|
+
const data = await response.json();
|
|
57
|
+
return JSON.stringify(data, null, 2);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return JSON.stringify({
|
|
61
|
+
error: `Network error: ${error.message}`
|
|
62
|
+
}, null, 2);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Fetch applications data
|
|
66
|
+
async function fetchApplications() {
|
|
67
|
+
const apiKey = getApiKey();
|
|
68
|
+
const apiUrl = getApiUrl();
|
|
69
|
+
if (!apiKey) {
|
|
70
|
+
return JSON.stringify({
|
|
71
|
+
error: "No API key configured",
|
|
72
|
+
help: "Get your API key at " + apiUrl + "/dashboard/api-keys"
|
|
73
|
+
}, null, 2);
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch(`${apiUrl}/api/applications`, {
|
|
77
|
+
headers: {
|
|
78
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
if (response.status === 401) {
|
|
83
|
+
return JSON.stringify({
|
|
84
|
+
error: "Invalid API key"
|
|
85
|
+
}, null, 2);
|
|
86
|
+
}
|
|
87
|
+
return JSON.stringify({
|
|
88
|
+
error: `Failed to fetch applications: ${response.status}`
|
|
89
|
+
}, null, 2);
|
|
90
|
+
}
|
|
91
|
+
const data = await response.json();
|
|
92
|
+
return JSON.stringify(data, null, 2);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return JSON.stringify({
|
|
96
|
+
error: `Network error: ${error.message}`
|
|
97
|
+
}, null, 2);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Handle resource reads
|
|
101
|
+
export async function handleReadResource(uri) {
|
|
102
|
+
switch (uri) {
|
|
103
|
+
case "jobseek://resume":
|
|
104
|
+
return {
|
|
105
|
+
contents: [{
|
|
106
|
+
uri,
|
|
107
|
+
mimeType: "application/json",
|
|
108
|
+
text: await fetchResume()
|
|
109
|
+
}]
|
|
110
|
+
};
|
|
111
|
+
case "jobseek://applications":
|
|
112
|
+
return {
|
|
113
|
+
contents: [{
|
|
114
|
+
uri,
|
|
115
|
+
mimeType: "application/json",
|
|
116
|
+
text: await fetchApplications()
|
|
117
|
+
}]
|
|
118
|
+
};
|
|
119
|
+
default:
|
|
120
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const launchPadTool: {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: string;
|
|
6
|
+
properties: {
|
|
7
|
+
query: {
|
|
8
|
+
type: string;
|
|
9
|
+
description: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
required: never[];
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export declare function handleLaunchPad(args: unknown, apiUrl: string, apiKey?: string): Promise<{
|
|
16
|
+
content: Array<{
|
|
17
|
+
type: string;
|
|
18
|
+
text: string;
|
|
19
|
+
}>;
|
|
20
|
+
}>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { exec } from "child_process";
|
|
3
|
+
import { promisify } from "util";
|
|
4
|
+
const execAsync = promisify(exec);
|
|
5
|
+
// Tool definition
|
|
6
|
+
export const launchPadTool = {
|
|
7
|
+
name: "launch_pad",
|
|
8
|
+
description: "Open job search tabs across major job boards (LinkedIn, Indeed, Glassdoor, ZipRecruiter, Wellfound, Dice). Uses your resume to determine the search query, or you can provide a custom query.",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: {
|
|
12
|
+
query: {
|
|
13
|
+
type: "string",
|
|
14
|
+
description: "Optional custom search query. If not provided, uses your most recent job title from your resume."
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
required: []
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
// Input validation
|
|
21
|
+
const LaunchPadInput = z.object({
|
|
22
|
+
query: z.string().optional()
|
|
23
|
+
});
|
|
24
|
+
// Tool handler
|
|
25
|
+
export async function handleLaunchPad(args, apiUrl, apiKey) {
|
|
26
|
+
const input = LaunchPadInput.parse(args);
|
|
27
|
+
if (!apiKey) {
|
|
28
|
+
return {
|
|
29
|
+
content: [{
|
|
30
|
+
type: "text",
|
|
31
|
+
text: `❌ **API Key Required**\n\nGet your API key at: ${apiUrl}/dashboard/api-keys`
|
|
32
|
+
}]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
// Fetch launchpad links from API
|
|
37
|
+
const queryParam = input.query ? `?query=${encodeURIComponent(input.query)}` : '';
|
|
38
|
+
const response = await fetch(`${apiUrl}/api/launchpad${queryParam}`, {
|
|
39
|
+
headers: {
|
|
40
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
if (response.status === 401) {
|
|
45
|
+
return {
|
|
46
|
+
content: [{
|
|
47
|
+
type: "text",
|
|
48
|
+
text: `❌ **Invalid API Key**\n\nGenerate a new key at: ${apiUrl}/dashboard/api-keys`
|
|
49
|
+
}]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const error = await response.json().catch(() => ({}));
|
|
53
|
+
return {
|
|
54
|
+
content: [{
|
|
55
|
+
type: "text",
|
|
56
|
+
text: `❌ **Error**\n\n${error.error || 'Failed to get launchpad links'}`
|
|
57
|
+
}]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const data = await response.json();
|
|
61
|
+
const { searchQuery, links } = data;
|
|
62
|
+
// Open each URL in the default browser
|
|
63
|
+
const opened = [];
|
|
64
|
+
for (const link of links) {
|
|
65
|
+
try {
|
|
66
|
+
// Works on macOS. For cross-platform: use 'start' on Windows, 'xdg-open' on Linux
|
|
67
|
+
await execAsync(`open "${link.url}"`);
|
|
68
|
+
opened.push(link.name);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
console.error(`Failed to open ${link.name}:`, err);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (opened.length === 0) {
|
|
75
|
+
return {
|
|
76
|
+
content: [{
|
|
77
|
+
type: "text",
|
|
78
|
+
text: `❌ **Failed to open tabs**\n\nCould not open browser tabs. You may need to open them manually.`
|
|
79
|
+
}]
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
content: [{
|
|
84
|
+
type: "text",
|
|
85
|
+
text: `🚀 **Launch Pad Activated!**
|
|
86
|
+
|
|
87
|
+
**Search Query:** ${searchQuery}
|
|
88
|
+
|
|
89
|
+
**Opened ${opened.length} job boards:**
|
|
90
|
+
${opened.map(name => `- ${name}`).join('\n')}
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
**Next steps:**
|
|
95
|
+
1. Browse the job listings
|
|
96
|
+
2. When you find interesting jobs, click the JobSeek extension to analyze them
|
|
97
|
+
3. Return here to discuss your matches: "What are my top matches from today?"`
|
|
98
|
+
}]
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return {
|
|
103
|
+
content: [{
|
|
104
|
+
type: "text",
|
|
105
|
+
text: `❌ **Error**\n\nFailed to launch job boards: ${error.message}`
|
|
106
|
+
}]
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|