jobseek-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 ADDED
@@ -0,0 +1,108 @@
1
+ # JobSeek MCP Server
2
+
3
+ AI-powered job search automation for Claude Code.
4
+
5
+ ## Quick Start
6
+
7
+ ### 1. Build the MCP Server
8
+ ```bash
9
+ cd jobseek-mcp
10
+ npm install
11
+ npm run build
12
+ ```
13
+
14
+ ### 2. Generate an API Key
15
+ - Go to https://getjobseek.com/dashboard/api-keys
16
+ - Click "Generate Key"
17
+ - Copy the key (you won't be able to see it again!)
18
+
19
+ ### 3. Configure Claude Code
20
+
21
+ Open `~/.claude.json` in your editor and find your project's `mcpServers` section. Add the `jobseek` entry:
22
+
23
+ ```json
24
+ {
25
+ "projects": {
26
+ "/path/to/your/project": {
27
+ "mcpServers": {
28
+ "jobseek": {
29
+ "type": "stdio",
30
+ "command": "node",
31
+ "args": ["/path/to/jobseek-mcp/dist/index.js"],
32
+ "env": {
33
+ "JOBSEEK_API_URL": "https://getjobseek.com",
34
+ "JOBSEEK_API_KEY": "sk_live_your_key_here"
35
+ }
36
+ }
37
+ }
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ **Important:** Replace:
44
+ - `/path/to/your/project` with your project directory
45
+ - `/path/to/jobseek-mcp/dist/index.js` with the actual path to the built MCP server
46
+ - `sk_live_your_key_here` with your generated API key
47
+
48
+ ### 4. Restart Claude Code
49
+
50
+ After saving the config, restart Claude Code. Type `/mcp` to verify the server is connected.
51
+
52
+ ## Available Tools
53
+
54
+ ### `optimize_resume`
55
+ Optimize your resume for ATS (Applicant Tracking Systems).
56
+
57
+ **Example prompts:**
58
+ - "Optimize my resume for ATS"
59
+ - "Regenerate my resume with a more technical summary"
60
+ - "Make my resume more concise"
61
+
62
+ **What it does:**
63
+ - Rewrites with stronger action verbs
64
+ - Adds measurable impact statements
65
+ - Optimizes keywords for ATS scanning
66
+ - Returns the optimized content + rationale
67
+
68
+ ### `download_resume_pdf`
69
+ Download your resume as a file.
70
+
71
+ **Example prompts:**
72
+ - "Download my resume"
73
+ - "Save my optimized resume to a file"
74
+ - "Download my ATS-optimized resume to ~/Documents/resume.txt"
75
+
76
+ **Parameters:**
77
+ - `filePath` (optional): Custom path for the file
78
+ - `optimized` (optional): If true, downloads the ATS-optimized version
79
+
80
+ ## Full Workflow Example
81
+
82
+ ```
83
+ You: "Optimize my resume for ATS"
84
+ Claude: [calls optimize_resume] "Here's your optimized resume..."
85
+
86
+ You: "That looks great! Download it as a file"
87
+ Claude: [calls download_resume_pdf] "Saved to ~/Downloads/resume_optimized.txt"
88
+ ```
89
+
90
+ ## Troubleshooting
91
+
92
+ ### "1 MCP server failed" error
93
+ - Check `/mcp` in Claude Code for details
94
+ - Verify the path to `dist/index.js` is correct
95
+ - Make sure you ran `npm run build`
96
+
97
+ ### "Authentication Required" error
98
+ - Verify your API key is correct in `~/.claude.json`
99
+ - Make sure you have a resume uploaded at https://getjobseek.com/dashboard/profile
100
+ - Check that JOBSEEK_API_URL is set to `https://getjobseek.com`
101
+
102
+ ## Development
103
+
104
+ ```bash
105
+ npm run dev # Run with tsx (hot reload)
106
+ npm run build # Build for production
107
+ npm start # Run production build
108
+ ```
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { optimizeResumeTool, handleOptimizeResume } from "./tools/optimize-resume.js";
6
+ import { downloadResumePdfTool, handleDownloadResumePdf } from "./tools/download-resume-pdf.js";
7
+ // Configuration
8
+ const JOBSEEK_API_URL = process.env.JOBSEEK_API_URL || "https://jobseek-iota.vercel.app";
9
+ const JOBSEEK_API_KEY = process.env.JOBSEEK_API_KEY;
10
+ // Create the MCP server
11
+ const server = new Server({
12
+ name: "jobseek-mcp",
13
+ version: "0.1.0",
14
+ }, {
15
+ capabilities: {
16
+ tools: {},
17
+ resources: {},
18
+ },
19
+ });
20
+ // List available tools
21
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
22
+ return {
23
+ tools: [
24
+ optimizeResumeTool,
25
+ downloadResumePdfTool,
26
+ ],
27
+ };
28
+ });
29
+ // Handle tool calls
30
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
31
+ const { name, arguments: args } = request.params;
32
+ switch (name) {
33
+ case "optimize_resume":
34
+ return handleOptimizeResume(args, JOBSEEK_API_URL, JOBSEEK_API_KEY);
35
+ case "download_resume_pdf":
36
+ return handleDownloadResumePdf(args, JOBSEEK_API_URL, JOBSEEK_API_KEY);
37
+ default:
38
+ throw new Error(`Unknown tool: ${name}`);
39
+ }
40
+ });
41
+ // List available resources
42
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
43
+ return {
44
+ resources: [
45
+ // Resources will be added here (resume, applications, etc.)
46
+ ],
47
+ };
48
+ });
49
+ // Handle resource reads
50
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
51
+ const { uri } = request.params;
52
+ throw new Error(`Unknown resource: ${uri}`);
53
+ });
54
+ // Start the server
55
+ async function main() {
56
+ const transport = new StdioServerTransport();
57
+ await server.connect(transport);
58
+ console.error("JobSeek MCP Server running on stdio");
59
+ }
60
+ main().catch(console.error);
@@ -0,0 +1,24 @@
1
+ export declare const downloadResumePdfTool: {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: "object";
6
+ properties: {
7
+ filePath: {
8
+ type: string;
9
+ description: string;
10
+ };
11
+ optimized: {
12
+ type: string;
13
+ description: string;
14
+ };
15
+ };
16
+ required: never[];
17
+ };
18
+ };
19
+ export declare function handleDownloadResumePdf(args: unknown, apiUrl: string, apiKey?: string): Promise<{
20
+ content: Array<{
21
+ type: string;
22
+ text: string;
23
+ }>;
24
+ }>;
@@ -0,0 +1,149 @@
1
+ import { z } from "zod";
2
+ import { writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ // Tool definition for MCP
5
+ export const downloadResumePdfTool = {
6
+ name: "download_resume_pdf",
7
+ description: `Generate and download a PDF of your resume.
8
+
9
+ This tool fetches your current resume from JobSeek and saves it as a PDF file.
10
+ You can optionally specify a custom file path, otherwise it will be saved to your Downloads folder.
11
+
12
+ Returns the path to the generated PDF file.`,
13
+ inputSchema: {
14
+ type: "object",
15
+ properties: {
16
+ filePath: {
17
+ type: "string",
18
+ description: "Optional custom file path for the PDF. Defaults to ~/Downloads/resume.pdf",
19
+ },
20
+ optimized: {
21
+ type: "boolean",
22
+ description: "If true, generates PDF from the ATS-optimized version. Otherwise uses current resume.",
23
+ },
24
+ },
25
+ required: [],
26
+ },
27
+ };
28
+ // Input validation schema
29
+ const DownloadResumePdfInput = z.object({
30
+ filePath: z.string().optional(),
31
+ optimized: z.boolean().optional().default(false),
32
+ });
33
+ // Simple PDF generation using plain text (no external dependencies)
34
+ function generateSimplePdf(resume) {
35
+ // Create a simple text-based "PDF" that most viewers can read
36
+ // For a real PDF, you'd use a library like pdfkit or jsPDF
37
+ const content = `
38
+ ================================================================================
39
+ RESUME
40
+ ================================================================================
41
+
42
+ ${resume.name}
43
+ ${resume.email}
44
+ ${resume.linkedinUrl ? `LinkedIn: ${resume.linkedinUrl}` : ''}
45
+
46
+ --------------------------------------------------------------------------------
47
+ PROFESSIONAL SUMMARY
48
+ --------------------------------------------------------------------------------
49
+ ${resume.summary || 'No summary available.'}
50
+
51
+ --------------------------------------------------------------------------------
52
+ SKILLS
53
+ --------------------------------------------------------------------------------
54
+ ${(resume.skills || []).join(' • ')}
55
+
56
+ --------------------------------------------------------------------------------
57
+ EXPERIENCE
58
+ --------------------------------------------------------------------------------
59
+ ${(resume.experience || []).map((exp) => `
60
+ ${exp.title} at ${exp.company}
61
+ ${exp.duration}
62
+
63
+ ${exp.description || (exp.bullets || []).map((b) => `• ${b}`).join('\n')}
64
+ `).join('\n')}
65
+
66
+ --------------------------------------------------------------------------------
67
+ EDUCATION
68
+ --------------------------------------------------------------------------------
69
+ ${(resume.education || []).map((edu) => `
70
+ ${edu.degree} - ${edu.institution} (${edu.year})
71
+ `).join('\n')}
72
+
73
+ ================================================================================
74
+ Generated by JobSeek
75
+ ================================================================================
76
+ `;
77
+ return Buffer.from(content, 'utf-8');
78
+ }
79
+ // Tool handler
80
+ export async function handleDownloadResumePdf(args, apiUrl, apiKey) {
81
+ const input = DownloadResumePdfInput.parse(args);
82
+ try {
83
+ const headers = {
84
+ "Content-Type": "application/json",
85
+ };
86
+ if (apiKey) {
87
+ headers["Authorization"] = `Bearer ${apiKey}`;
88
+ }
89
+ // If optimized, first call optimize endpoint, otherwise fetch current resume
90
+ const endpoint = input.optimized
91
+ ? `${apiUrl}/api/resumes/optimize`
92
+ : `${apiUrl}/api/resumes/current`;
93
+ const response = await fetch(endpoint, {
94
+ method: input.optimized ? "POST" : "GET",
95
+ headers,
96
+ ...(input.optimized ? { body: JSON.stringify({}) } : {}),
97
+ });
98
+ if (!response.ok) {
99
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
100
+ if (response.status === 401) {
101
+ return {
102
+ content: [
103
+ {
104
+ type: "text",
105
+ text: "❌ **Authentication Required**\n\nPlease set your JOBSEEK_API_KEY environment variable.",
106
+ },
107
+ ],
108
+ };
109
+ }
110
+ throw new Error(error.error || `API error: ${response.status}`);
111
+ }
112
+ const resumeData = await response.json();
113
+ // For optimized resume, the data is in result.content
114
+ const resume = input.optimized ? resumeData.content : resumeData;
115
+ // Generate PDF content
116
+ const pdfBuffer = generateSimplePdf(resume);
117
+ // Determine file path
118
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '.';
119
+ const defaultPath = join(homeDir, 'Downloads', `resume${input.optimized ? '_optimized' : ''}.txt`);
120
+ const outputPath = input.filePath || defaultPath;
121
+ // Write to file
122
+ writeFileSync(outputPath, pdfBuffer);
123
+ return {
124
+ content: [
125
+ {
126
+ type: "text",
127
+ text: `✅ **Resume Downloaded!**
128
+
129
+ Your resume has been saved to:
130
+ \`${outputPath}\`
131
+
132
+ ${input.optimized ? '📈 This is your ATS-optimized version.' : '📄 This is your current resume.'}
133
+
134
+ **Note:** This is a text-formatted resume. For a properly formatted PDF, visit your JobSeek dashboard and use the "Download PDF" button.`,
135
+ },
136
+ ],
137
+ };
138
+ }
139
+ catch (error) {
140
+ return {
141
+ content: [
142
+ {
143
+ type: "text",
144
+ text: `❌ **Error generating resume:** ${error.message}`,
145
+ },
146
+ ],
147
+ };
148
+ }
149
+ }
@@ -0,0 +1,20 @@
1
+ export declare const optimizeResumeTool: {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: "object";
6
+ properties: {
7
+ feedback: {
8
+ type: string;
9
+ description: string;
10
+ };
11
+ };
12
+ required: never[];
13
+ };
14
+ };
15
+ export declare function handleOptimizeResume(args: unknown, apiUrl: string, apiKey?: string): Promise<{
16
+ content: Array<{
17
+ type: string;
18
+ text: string;
19
+ }>;
20
+ }>;
@@ -0,0 +1,114 @@
1
+ import { z } from "zod";
2
+ // Tool definition for MCP
3
+ export const optimizeResumeTool = {
4
+ name: "optimize_resume",
5
+ description: `Optimize a resume for ATS (Applicant Tracking Systems).
6
+
7
+ This tool rewrites your resume to:
8
+ - Use strong action verbs
9
+ - Add measurable impact statements
10
+ - Optimize keywords for ATS scanning
11
+ - Improve formatting for machine readability
12
+
13
+ Returns the optimized resume content and a rationale explaining the changes.`,
14
+ inputSchema: {
15
+ type: "object",
16
+ properties: {
17
+ feedback: {
18
+ type: "string",
19
+ description: "Optional feedback for regeneration (e.g., 'make the summary more technical')",
20
+ },
21
+ },
22
+ required: [],
23
+ },
24
+ };
25
+ // Input validation schema
26
+ const OptimizeResumeInput = z.object({
27
+ feedback: z.string().optional(),
28
+ });
29
+ // Tool handler
30
+ export async function handleOptimizeResume(args, apiUrl, apiKey) {
31
+ const input = OptimizeResumeInput.parse(args);
32
+ try {
33
+ const headers = {
34
+ "Content-Type": "application/json",
35
+ };
36
+ if (apiKey) {
37
+ headers["Authorization"] = `Bearer ${apiKey}`;
38
+ }
39
+ const response = await fetch(`${apiUrl}/api/resumes/optimize`, {
40
+ method: "POST",
41
+ headers,
42
+ body: JSON.stringify({
43
+ feedback: input.feedback,
44
+ }),
45
+ });
46
+ if (!response.ok) {
47
+ const error = await response.json().catch(() => ({ error: "Unknown error" }));
48
+ if (response.status === 401) {
49
+ return {
50
+ content: [
51
+ {
52
+ type: "text",
53
+ text: "❌ **Authentication Required**\n\nPlease log in to JobSeek first:\n1. Visit https://jobseek-iota.vercel.app\n2. Sign in with Google\n3. Upload your resume\n4. Try this tool again",
54
+ },
55
+ ],
56
+ };
57
+ }
58
+ throw new Error(error.error || `API error: ${response.status}`);
59
+ }
60
+ const result = await response.json();
61
+ // Format the response nicely
62
+ const formattedResponse = `# ✨ ATS-Optimized Resume
63
+
64
+ ## Professional Summary
65
+ ${result.content.summary}
66
+
67
+ ## Skills
68
+ ${result.content.skills.map((s) => `- ${s}`).join("\n")}
69
+
70
+ ## Experience
71
+ ${result.content.experience
72
+ .map((exp) => `### ${exp.title} at ${exp.company}
73
+ *${exp.duration}*
74
+
75
+ ${exp.description}`)
76
+ .join("\n\n")}
77
+
78
+ ## Education
79
+ ${result.content.education
80
+ .map((edu) => `- **${edu.degree}** - ${edu.institution} (${edu.year})`)
81
+ .join("\n")}
82
+
83
+ ---
84
+
85
+ ## 📋 Changes Made
86
+
87
+ **Summary Changes:** ${result.rationale.summaryChanges}
88
+
89
+ **Skill Prioritization:** ${result.rationale.skillPrioritization}
90
+
91
+ **Experience Improvements:** ${result.rationale.experienceHighlights || result.rationale.experienceImprovements}
92
+
93
+ **Formatting Changes:** ${result.rationale.formattingChanges}
94
+ `;
95
+ return {
96
+ content: [
97
+ {
98
+ type: "text",
99
+ text: formattedResponse,
100
+ },
101
+ ],
102
+ };
103
+ }
104
+ catch (error) {
105
+ return {
106
+ content: [
107
+ {
108
+ type: "text",
109
+ text: `❌ **Error optimizing resume:** ${error.message}`,
110
+ },
111
+ ],
112
+ };
113
+ }
114
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "jobseek-mcp",
3
+ "version": "0.1.0",
4
+ "description": "JobSeek MCP Server - AI-powered job search automation for Claude Code",
5
+ "author": "Shawn Mitchell",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/shawnmitchell/jobseek.git"
10
+ },
11
+ "keywords": [
12
+ "mcp",
13
+ "claude",
14
+ "ai",
15
+ "resume",
16
+ "job-search",
17
+ "ats",
18
+ "model-context-protocol"
19
+ ],
20
+ "type": "module",
21
+ "main": "dist/index.js",
22
+ "bin": {
23
+ "jobseek-mcp": "dist/index.js"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "README.md"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsc",
31
+ "dev": "tsx src/index.ts",
32
+ "start": "node dist/index.js",
33
+ "prepublishOnly": "npm run build"
34
+ },
35
+ "dependencies": {
36
+ "@modelcontextprotocol/sdk": "^0.6.0",
37
+ "zod": "^3.22.4"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^20.10.0",
41
+ "tsx": "^4.7.0",
42
+ "typescript": "^5.3.0"
43
+ },
44
+ "engines": {
45
+ "node": ">=18"
46
+ }
47
+ }