jobseek-mcp 0.1.0 → 0.2.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/dist/index.js +5 -1
- package/dist/tools/upload-resume.d.ts +20 -0
- package/dist/tools/upload-resume.js +120 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -4,13 +4,14 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { optimizeResumeTool, handleOptimizeResume } from "./tools/optimize-resume.js";
|
|
6
6
|
import { downloadResumePdfTool, handleDownloadResumePdf } from "./tools/download-resume-pdf.js";
|
|
7
|
+
import { uploadResumeTool, handleUploadResume } from "./tools/upload-resume.js";
|
|
7
8
|
// Configuration
|
|
8
9
|
const JOBSEEK_API_URL = process.env.JOBSEEK_API_URL || "https://jobseek-iota.vercel.app";
|
|
9
10
|
const JOBSEEK_API_KEY = process.env.JOBSEEK_API_KEY;
|
|
10
11
|
// Create the MCP server
|
|
11
12
|
const server = new Server({
|
|
12
13
|
name: "jobseek-mcp",
|
|
13
|
-
version: "0.
|
|
14
|
+
version: "0.2.0",
|
|
14
15
|
}, {
|
|
15
16
|
capabilities: {
|
|
16
17
|
tools: {},
|
|
@@ -23,6 +24,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
23
24
|
tools: [
|
|
24
25
|
optimizeResumeTool,
|
|
25
26
|
downloadResumePdfTool,
|
|
27
|
+
uploadResumeTool,
|
|
26
28
|
],
|
|
27
29
|
};
|
|
28
30
|
});
|
|
@@ -34,6 +36,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
34
36
|
return handleOptimizeResume(args, JOBSEEK_API_URL, JOBSEEK_API_KEY);
|
|
35
37
|
case "download_resume_pdf":
|
|
36
38
|
return handleDownloadResumePdf(args, JOBSEEK_API_URL, JOBSEEK_API_KEY);
|
|
39
|
+
case "upload_resume":
|
|
40
|
+
return handleUploadResume(args, JOBSEEK_API_URL, JOBSEEK_API_KEY);
|
|
37
41
|
default:
|
|
38
42
|
throw new Error(`Unknown tool: ${name}`);
|
|
39
43
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const uploadResumeTool: {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: string;
|
|
6
|
+
properties: {
|
|
7
|
+
filePath: {
|
|
8
|
+
type: string;
|
|
9
|
+
description: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
required: string[];
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export declare function handleUploadResume(args: unknown, apiUrl: string, apiKey?: string): Promise<{
|
|
16
|
+
content: Array<{
|
|
17
|
+
type: string;
|
|
18
|
+
text: string;
|
|
19
|
+
}>;
|
|
20
|
+
}>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
// Tool definition
|
|
5
|
+
export const uploadResumeTool = {
|
|
6
|
+
name: "upload_resume",
|
|
7
|
+
description: "Upload a resume PDF to JobSeek. The resume will be parsed and stored, enabling optimization and job matching features. Accepts a file path to a PDF file.",
|
|
8
|
+
inputSchema: {
|
|
9
|
+
type: "object",
|
|
10
|
+
properties: {
|
|
11
|
+
filePath: {
|
|
12
|
+
type: "string",
|
|
13
|
+
description: "Absolute path to the PDF file to upload (e.g., /Users/name/Documents/resume.pdf or ~/Downloads/resume.pdf)"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
required: ["filePath"]
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
// Input validation
|
|
20
|
+
const UploadResumeInput = z.object({
|
|
21
|
+
filePath: z.string().min(1, "File path is required")
|
|
22
|
+
});
|
|
23
|
+
// Expand ~ to home directory
|
|
24
|
+
function expandPath(filePath) {
|
|
25
|
+
if (filePath.startsWith('~')) {
|
|
26
|
+
return path.join(process.env.HOME || '', filePath.slice(1));
|
|
27
|
+
}
|
|
28
|
+
return filePath;
|
|
29
|
+
}
|
|
30
|
+
// Tool handler
|
|
31
|
+
export async function handleUploadResume(args, apiUrl, apiKey) {
|
|
32
|
+
const input = UploadResumeInput.parse(args);
|
|
33
|
+
const expandedPath = expandPath(input.filePath);
|
|
34
|
+
// Validate file exists
|
|
35
|
+
if (!fs.existsSync(expandedPath)) {
|
|
36
|
+
return {
|
|
37
|
+
content: [{
|
|
38
|
+
type: "text",
|
|
39
|
+
text: `❌ **File Not Found**\n\nCould not find file: ${input.filePath}\n\nPlease check the path and try again.`
|
|
40
|
+
}]
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
// Validate it's a PDF
|
|
44
|
+
if (!expandedPath.toLowerCase().endsWith('.pdf')) {
|
|
45
|
+
return {
|
|
46
|
+
content: [{
|
|
47
|
+
type: "text",
|
|
48
|
+
text: `❌ **Invalid File Type**\n\nOnly PDF files are supported. Please provide a .pdf file.`
|
|
49
|
+
}]
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Read file and convert to base64
|
|
53
|
+
const fileBuffer = fs.readFileSync(expandedPath);
|
|
54
|
+
const base64Data = fileBuffer.toString('base64');
|
|
55
|
+
const fileName = path.basename(expandedPath);
|
|
56
|
+
try {
|
|
57
|
+
const headers = {
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
};
|
|
60
|
+
if (apiKey) {
|
|
61
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
62
|
+
}
|
|
63
|
+
const response = await fetch(`${apiUrl}/api/resumes/upload`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers,
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
pdfBase64: base64Data,
|
|
68
|
+
fileName
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
const error = await response.json().catch(() => ({ error: "Unknown error" }));
|
|
73
|
+
if (response.status === 401) {
|
|
74
|
+
return {
|
|
75
|
+
content: [{
|
|
76
|
+
type: "text",
|
|
77
|
+
text: `❌ **Authentication Required**\n\nPlease configure your API key. Get one at: ${apiUrl}/dashboard/api-keys`
|
|
78
|
+
}]
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
content: [{
|
|
83
|
+
type: "text",
|
|
84
|
+
text: `❌ **Upload Failed**\n\n${error.error || error.message || 'Unknown error'}`
|
|
85
|
+
}]
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const result = await response.json();
|
|
89
|
+
return {
|
|
90
|
+
content: [{
|
|
91
|
+
type: "text",
|
|
92
|
+
text: `✅ **Resume Uploaded Successfully!**
|
|
93
|
+
|
|
94
|
+
**File:** ${result.fileName}
|
|
95
|
+
|
|
96
|
+
**Parsed Data:**
|
|
97
|
+
- Skills detected: ${result.skillsCount}
|
|
98
|
+
- Experience entries: ${result.experienceCount}
|
|
99
|
+
|
|
100
|
+
**Summary:**
|
|
101
|
+
${result.summary || 'No summary extracted'}
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
Your resume is now stored in JobSeek. You can:
|
|
106
|
+
- Run "optimize my resume for ATS" to improve it
|
|
107
|
+
- Analyze job postings for match scores
|
|
108
|
+
- Download optimized versions`
|
|
109
|
+
}]
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
return {
|
|
114
|
+
content: [{
|
|
115
|
+
type: "text",
|
|
116
|
+
text: `❌ **Error**\n\nFailed to upload resume: ${error.message}`
|
|
117
|
+
}]
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jobseek-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "JobSeek MCP Server - AI-powered job search automation for Claude Code",
|
|
5
5
|
"author": "Shawn Mitchell",
|
|
6
6
|
"license": "MIT",
|
|
@@ -44,4 +44,4 @@
|
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=18"
|
|
46
46
|
}
|
|
47
|
-
}
|
|
47
|
+
}
|