jobseek-mcp 0.1.0 → 0.3.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 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
@@ -4,13 +4,15 @@ 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";
8
+ import { allResources, handleReadResource } from "./resources.js";
7
9
  // Configuration
8
10
  const JOBSEEK_API_URL = process.env.JOBSEEK_API_URL || "https://jobseek-iota.vercel.app";
9
11
  const JOBSEEK_API_KEY = process.env.JOBSEEK_API_KEY;
10
12
  // Create the MCP server
11
13
  const server = new Server({
12
14
  name: "jobseek-mcp",
13
- version: "0.1.0",
15
+ version: "0.3.0",
14
16
  }, {
15
17
  capabilities: {
16
18
  tools: {},
@@ -23,6 +25,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
23
25
  tools: [
24
26
  optimizeResumeTool,
25
27
  downloadResumePdfTool,
28
+ uploadResumeTool,
26
29
  ],
27
30
  };
28
31
  });
@@ -34,6 +37,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
34
37
  return handleOptimizeResume(args, JOBSEEK_API_URL, JOBSEEK_API_KEY);
35
38
  case "download_resume_pdf":
36
39
  return handleDownloadResumePdf(args, JOBSEEK_API_URL, JOBSEEK_API_KEY);
40
+ case "upload_resume":
41
+ return handleUploadResume(args, JOBSEEK_API_URL, JOBSEEK_API_KEY);
37
42
  default:
38
43
  throw new Error(`Unknown tool: ${name}`);
39
44
  }
@@ -41,15 +46,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
41
46
  // List available resources
42
47
  server.setRequestHandler(ListResourcesRequestSchema, async () => {
43
48
  return {
44
- resources: [
45
- // Resources will be added here (resume, applications, etc.)
46
- ],
49
+ resources: allResources,
47
50
  };
48
51
  });
49
52
  // Handle resource reads
50
53
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
51
54
  const { uri } = request.params;
52
- throw new Error(`Unknown resource: ${uri}`);
55
+ return handleReadResource(uri);
53
56
  });
54
57
  // Start the server
55
58
  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 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.1.0",
3
+ "version": "0.3.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
+ }