@sindhujaks/career-mcp 0.0.0-stage → 1.0.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/.env.example +20 -0
- package/LICENSE +21 -0
- package/README.md +77 -2
- package/dist/cli.js +124 -0
- package/dist/index.js +245 -0
- package/dist/interview.js +79 -0
- package/dist/llm-fallback.js +64 -0
- package/dist/storage.js +150 -0
- package/package.json +56 -4
package/.env.example
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# career-mcp — environment variables
|
|
2
|
+
# Copy this file to .env and fill in your values. Never commit .env.
|
|
3
|
+
#
|
|
4
|
+
# Only needed if you're using the standalone CLI (career-mcp-cli) without an
|
|
5
|
+
# MCP-capable client. The MCP server itself (career-mcp) needs none of this —
|
|
6
|
+
# it relies on the host LLM (Claude Code, Claude Desktop, etc.) to reason.
|
|
7
|
+
|
|
8
|
+
# ─── Optional — where your resume/stories/applications are stored ────────────
|
|
9
|
+
# Defaults to ~/.career-mcp
|
|
10
|
+
CAREER_MCP_DATA_DIR=
|
|
11
|
+
|
|
12
|
+
# ─── Required for the standalone CLI only ─────────────────────────────────────
|
|
13
|
+
# Bring your own key — career-mcp never calls a shared/hosted key, so there's
|
|
14
|
+
# no cost or liability on anyone but you.
|
|
15
|
+
#
|
|
16
|
+
# "groq" — free tier at https://console.groq.com/keys
|
|
17
|
+
# "gemini" — free tier at https://aistudio.google.com/apikey
|
|
18
|
+
CAREER_MCP_LLM_PROVIDER=groq
|
|
19
|
+
CAREER_MCP_API_KEY=
|
|
20
|
+
# CAREER_MCP_LLM_MODEL= # optional override, defaults per provider
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sindhuja Kalisrinivasan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,78 @@
|
|
|
1
|
-
#
|
|
1
|
+
# career-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An MCP server for the job-search loop: store a resume, drop in a job description, get a real match analysis, a tailored resume, a cover letter, a LinkedIn About section — plus a structured interview that captures your career stories so they're ready to reuse.
|
|
4
|
+
|
|
5
|
+
Local-first. Your resume and stories live in a folder on your machine (`~/.career-mcp` by default) — nothing is uploaded anywhere.
|
|
6
|
+
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
The MCP server itself doesn't call any LLM. It's a data layer: it stores your resume, job descriptions, voice profile, and stories, and hands them back as context. The actual reasoning — match analysis, tailoring, writing — is done by whatever MCP-capable client you're using (Claude Code, Claude Desktop, etc.), the same way any tool-using conversation works.
|
|
10
|
+
|
|
11
|
+
If you don't have an MCP client, the bundled CLI (`career-mcp-cli`) does the same job standalone, using your own Groq or Gemini API key (both have usable free tiers) to do the reasoning itself.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g @sindhujaks/career-mcp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Use with an MCP client (recommended)
|
|
20
|
+
|
|
21
|
+
Add to your client's MCP config (e.g. `.mcp.json` for Claude Code):
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"career-mcp": {
|
|
27
|
+
"command": "career-mcp"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Then, in conversation: save your resume, save a JD, and ask your client to run a match analysis, draft a tailored resume, write a cover letter, or draft a LinkedIn About. Say "let's capture a career story" to run the guided interview.
|
|
34
|
+
|
|
35
|
+
### Tools
|
|
36
|
+
|
|
37
|
+
| Tool | Purpose |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `save_resume` / `get_resume` | Store/retrieve your base resume |
|
|
40
|
+
| `save_voice_profile` / `get_voice_profile` | Store tone/style guidance so generated content sounds like you |
|
|
41
|
+
| `save_jd` | Store a job description, sets it as the active application |
|
|
42
|
+
| `get_active_application` | Bundles resume + JD + voice profile + relevant stories as context |
|
|
43
|
+
| `list_applications` | List all stored applications |
|
|
44
|
+
| `save_output` | Persist a generated match analysis / resume / cover letter / About section |
|
|
45
|
+
| `save_story` / `query_stories` | Save/retrieve career stories, filterable by tag |
|
|
46
|
+
| `start_story_interview` / `answer_story_question` | Guided, protocol-driven interview to capture one new story |
|
|
47
|
+
|
|
48
|
+
## Use standalone (no MCP client)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
cp .env.example .env # fill in CAREER_MCP_API_KEY (free Groq or Gemini key)
|
|
52
|
+
|
|
53
|
+
career-mcp-cli set-resume --file resume.txt
|
|
54
|
+
career-mcp-cli set-jd --company Acme --role "Senior Engineer" --file jd.txt
|
|
55
|
+
career-mcp-cli analyze
|
|
56
|
+
career-mcp-cli resume
|
|
57
|
+
career-mcp-cli cover-letter
|
|
58
|
+
career-mcp-cli about
|
|
59
|
+
career-mcp-cli interview # guided story capture
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Data layout
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
~/.career-mcp/
|
|
66
|
+
├── resume.json
|
|
67
|
+
├── voice_profile.md
|
|
68
|
+
├── stories/<slug>.md
|
|
69
|
+
└── applications/<company-role>/
|
|
70
|
+
├── jd.json
|
|
71
|
+
└── outputs/{match_analysis,tailored_resume,cover_letter,linkedin_about}.md
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Override the location with `CAREER_MCP_DATA_DIR`.
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as readline from "node:readline/promises";
|
|
3
|
+
import { stdin, stdout } from "node:process";
|
|
4
|
+
import "dotenv/config";
|
|
5
|
+
import * as storage from "./storage.js";
|
|
6
|
+
import { startStoryInterview, answerStoryQuestion } from "./interview.js";
|
|
7
|
+
import { callLLM } from "./llm-fallback.js";
|
|
8
|
+
const SYSTEM_PROMPT = "You are a career writing assistant. Write in a clear, direct, honest voice — " +
|
|
9
|
+
"no buzzwords, no AI-sounding phrases ('in today's fast-paced world', 'as we navigate'), " +
|
|
10
|
+
"no fake inspiration. Prefer specific, grounded claims over impressive-sounding generalities. " +
|
|
11
|
+
"Never fabricate experience that isn't in the provided resume or stories.";
|
|
12
|
+
async function readArgFile(flagValue) {
|
|
13
|
+
if (!flagValue)
|
|
14
|
+
return undefined;
|
|
15
|
+
return storage.readTextFile(flagValue);
|
|
16
|
+
}
|
|
17
|
+
function parseFlags(argv) {
|
|
18
|
+
const flags = {};
|
|
19
|
+
for (let i = 0; i < argv.length; i++) {
|
|
20
|
+
if (argv[i].startsWith("--")) {
|
|
21
|
+
const key = argv[i].slice(2);
|
|
22
|
+
const value = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : "true";
|
|
23
|
+
flags[key] = value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return flags;
|
|
27
|
+
}
|
|
28
|
+
async function buildContext() {
|
|
29
|
+
const [resume, voice, application] = await Promise.all([
|
|
30
|
+
storage.getResume(),
|
|
31
|
+
storage.getVoiceProfile(),
|
|
32
|
+
storage.getActiveApplication(),
|
|
33
|
+
]);
|
|
34
|
+
if (!application)
|
|
35
|
+
throw new Error("No active application. Run `career-mcp-cli set-jd` first.");
|
|
36
|
+
const stories = await storage.queryStories();
|
|
37
|
+
return { resume, voice, application, stories };
|
|
38
|
+
}
|
|
39
|
+
function contextToPrompt(ctx) {
|
|
40
|
+
return [
|
|
41
|
+
`Job description (${ctx.application.company} — ${ctx.application.role}):`,
|
|
42
|
+
ctx.application.jdText,
|
|
43
|
+
"",
|
|
44
|
+
"Resume:",
|
|
45
|
+
ctx.resume?.rawText ?? "(none provided)",
|
|
46
|
+
"",
|
|
47
|
+
"Voice profile:",
|
|
48
|
+
ctx.voice ?? "(none provided — default to clear, direct, honest)",
|
|
49
|
+
ctx.stories.length > 0 ? `\nCareer stories (use if relevant):\n${ctx.stories.map((s) => `${s.title}: ${s.content}`).join("\n\n")}` : "",
|
|
50
|
+
].join("\n");
|
|
51
|
+
}
|
|
52
|
+
async function cmdSetResume(flags) {
|
|
53
|
+
const text = await readArgFile(flags.file);
|
|
54
|
+
if (!text)
|
|
55
|
+
throw new Error("Usage: career-mcp-cli set-resume --file <path>");
|
|
56
|
+
await storage.saveResume(text);
|
|
57
|
+
console.log("Resume saved.");
|
|
58
|
+
}
|
|
59
|
+
async function cmdSetJD(flags) {
|
|
60
|
+
const text = await readArgFile(flags.file);
|
|
61
|
+
if (!text || !flags.company || !flags.role) {
|
|
62
|
+
throw new Error("Usage: career-mcp-cli set-jd --company <name> --role <title> --file <path>");
|
|
63
|
+
}
|
|
64
|
+
const application = await storage.saveJD(flags.company, flags.role, text);
|
|
65
|
+
console.log(`Saved JD for ${flags.company} — ${flags.role} (${application.slug}), set as active.`);
|
|
66
|
+
}
|
|
67
|
+
async function cmdGenerate(outputType, instruction) {
|
|
68
|
+
const ctx = await buildContext();
|
|
69
|
+
const prompt = `${instruction}\n\n${contextToPrompt(ctx)}`;
|
|
70
|
+
const result = await callLLM(SYSTEM_PROMPT, prompt);
|
|
71
|
+
console.log(result);
|
|
72
|
+
await storage.saveOutput(ctx.application.slug, outputType, result);
|
|
73
|
+
console.log(`\n(saved to ~/.career-mcp/applications/${ctx.application.slug}/outputs/${outputType}.md)`);
|
|
74
|
+
}
|
|
75
|
+
async function cmdInterview() {
|
|
76
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
77
|
+
let step = startStoryInterview();
|
|
78
|
+
console.log(`Let's capture a story. ${step.total_questions} quick questions.\n`);
|
|
79
|
+
while (step.status === "in_progress") {
|
|
80
|
+
const answer = await rl.question(`${step.question}\n> `);
|
|
81
|
+
step = await answerStoryQuestion(step.session_id, answer);
|
|
82
|
+
console.log("");
|
|
83
|
+
}
|
|
84
|
+
console.log(`Saved: "${step.story?.title}" [${step.story?.tags.join(", ")}]`);
|
|
85
|
+
rl.close();
|
|
86
|
+
}
|
|
87
|
+
async function main() {
|
|
88
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
89
|
+
const flags = parseFlags(rest);
|
|
90
|
+
switch (command) {
|
|
91
|
+
case "set-resume":
|
|
92
|
+
return cmdSetResume(flags);
|
|
93
|
+
case "set-jd":
|
|
94
|
+
return cmdSetJD(flags);
|
|
95
|
+
case "interview":
|
|
96
|
+
return cmdInterview();
|
|
97
|
+
case "analyze":
|
|
98
|
+
return cmdGenerate("match_analysis", "Analyze how well this resume matches this job description. Call out genuine overlaps and honest gaps — no keyword stuffing.");
|
|
99
|
+
case "resume":
|
|
100
|
+
return cmdGenerate("tailored_resume", "Produce a tailored version of this resume for this job description, reordering and reframing existing content for relevance. Do not invent experience.");
|
|
101
|
+
case "cover-letter":
|
|
102
|
+
return cmdGenerate("cover_letter", "Write a cover letter for this job description, in the voice profile's tone. A real letter: date, Dear Hiring Team, body, Sincerely + name.");
|
|
103
|
+
case "about":
|
|
104
|
+
return cmdGenerate("linkedin_about", "Write a LinkedIn About section based on this resume and these stories, in the voice profile's tone.");
|
|
105
|
+
default:
|
|
106
|
+
console.log([
|
|
107
|
+
"career-mcp-cli — standalone mode (no MCP client needed)",
|
|
108
|
+
"Requires CAREER_MCP_API_KEY in your environment (Groq or Gemini free tier). See .env.example.",
|
|
109
|
+
"",
|
|
110
|
+
"Commands:",
|
|
111
|
+
" set-resume --file <path>",
|
|
112
|
+
" set-jd --company <name> --role <title> --file <path>",
|
|
113
|
+
" interview",
|
|
114
|
+
" analyze",
|
|
115
|
+
" resume",
|
|
116
|
+
" cover-letter",
|
|
117
|
+
" about",
|
|
118
|
+
].join("\n"));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
main().catch((err) => {
|
|
122
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
});
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
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, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import * as storage from "./storage.js";
|
|
6
|
+
import { startStoryInterview, answerStoryQuestion } from "./interview.js";
|
|
7
|
+
const server = new Server({ name: "career-mcp", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
8
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
9
|
+
tools: [
|
|
10
|
+
{
|
|
11
|
+
name: "save_resume",
|
|
12
|
+
description: "Store the user's base resume, either as raw text or by reading a local file. Overwrites any previously stored resume. Provide exactly one of resume_text or resume_path.",
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
resume_text: { type: "string", description: "The full resume content" },
|
|
17
|
+
resume_path: { type: "string", description: "Absolute path to a .pdf, .txt, or .md resume file to read instead of passing raw text" },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: "get_resume",
|
|
23
|
+
description: "Retrieve the stored base resume.",
|
|
24
|
+
inputSchema: { type: "object", properties: {} },
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "save_voice_profile",
|
|
28
|
+
description: "Store a description of the user's writing voice/tone (e.g. tone attributes, phrases to avoid, sample posts). Used to keep generated cover letters and LinkedIn About sections sounding like the user, not generic AI copy.",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
voice_text: { type: "string", description: "Description of tone, style, and things to avoid" },
|
|
33
|
+
},
|
|
34
|
+
required: ["voice_text"],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "get_voice_profile",
|
|
39
|
+
description: "Retrieve the stored voice profile, if one has been saved.",
|
|
40
|
+
inputSchema: { type: "object", properties: {} },
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: "save_jd",
|
|
44
|
+
description: "Store a job description for a company/role and mark it as the active application. Creates a new application record. Provide exactly one of jd_text or jd_path.",
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: {
|
|
48
|
+
company: { type: "string" },
|
|
49
|
+
role: { type: "string" },
|
|
50
|
+
jd_text: { type: "string", description: "The full job description text" },
|
|
51
|
+
jd_path: { type: "string", description: "Absolute path to a .pdf, .txt, or .md job description file to read instead of passing raw text" },
|
|
52
|
+
},
|
|
53
|
+
required: ["company", "role"],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "get_active_application",
|
|
58
|
+
description: "Returns the stored resume, voice profile, and the active job description together, as context for match analysis or content generation. Use this to: (1) run match analysis — identify genuine overlaps and honest gaps between the resume and JD, not keyword stuffing; (2) draft a tailored resume by reordering/reframing existing resume content for relevance, never fabricating experience; (3) draft a cover letter; (4) draft a LinkedIn About section. Match the voice profile's tone if one is set. Save any generated output with save_output.",
|
|
59
|
+
inputSchema: { type: "object", properties: {} },
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "list_applications",
|
|
63
|
+
description: "List all stored applications (company, role, created date).",
|
|
64
|
+
inputSchema: { type: "object", properties: {} },
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "save_output",
|
|
68
|
+
description: "Persist generated content (match analysis, tailored resume, cover letter, or LinkedIn About draft) for an application. Defaults to the active application if application_slug is omitted.",
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: "object",
|
|
71
|
+
properties: {
|
|
72
|
+
output_type: {
|
|
73
|
+
type: "string",
|
|
74
|
+
enum: ["match_analysis", "tailored_resume", "cover_letter", "linkedin_about"],
|
|
75
|
+
},
|
|
76
|
+
content: { type: "string" },
|
|
77
|
+
application_slug: { type: "string", description: "Optional — defaults to the active application" },
|
|
78
|
+
},
|
|
79
|
+
required: ["output_type", "content"],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: "save_story",
|
|
84
|
+
description: "Save a career story directly (title, tags, content) without running the interview flow. Prefer start_story_interview for a guided capture.",
|
|
85
|
+
inputSchema: {
|
|
86
|
+
type: "object",
|
|
87
|
+
properties: {
|
|
88
|
+
title: { type: "string" },
|
|
89
|
+
tags: { type: "array", items: { type: "string" } },
|
|
90
|
+
content: { type: "string" },
|
|
91
|
+
},
|
|
92
|
+
required: ["title", "tags", "content"],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "query_stories",
|
|
97
|
+
description: "Retrieve saved career stories, optionally filtered by tag (e.g. leadership, conflict, failure-recovery, technical-deep-dive). Use to find a story relevant to a JD requirement, an interview-prep question, or a LinkedIn post idea.",
|
|
98
|
+
inputSchema: {
|
|
99
|
+
type: "object",
|
|
100
|
+
properties: {
|
|
101
|
+
tags: { type: "array", items: { type: "string" }, description: "Optional tag filter" },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "start_story_interview",
|
|
107
|
+
description: "Begin a structured interview to capture one new career story. Returns the first question and a session_id. Keep calling answer_story_question with that session_id, relaying each returned question to the user verbatim, until status is 'complete' — the story is then auto-saved.",
|
|
108
|
+
inputSchema: { type: "object", properties: {} },
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "answer_story_question",
|
|
112
|
+
description: "Submit the user's answer to the current interview question and get the next one (or the completed, saved story).",
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: {
|
|
116
|
+
session_id: { type: "string" },
|
|
117
|
+
answer: { type: "string" },
|
|
118
|
+
},
|
|
119
|
+
required: ["session_id", "answer"],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
}));
|
|
124
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
125
|
+
const { name, arguments: args } = request.params;
|
|
126
|
+
try {
|
|
127
|
+
switch (name) {
|
|
128
|
+
case "save_resume": {
|
|
129
|
+
const { resume_text, resume_path } = args;
|
|
130
|
+
const text = resume_text ?? (resume_path ? await storage.readTextFile(resume_path) : undefined);
|
|
131
|
+
if (!text)
|
|
132
|
+
throw new Error("Provide either resume_text or resume_path.");
|
|
133
|
+
await storage.saveResume(text);
|
|
134
|
+
return textResult("Resume saved.");
|
|
135
|
+
}
|
|
136
|
+
case "get_resume": {
|
|
137
|
+
const resume = await storage.getResume();
|
|
138
|
+
return textResult(resume?.rawText ? resume.rawText : "No resume stored yet. Use save_resume first.");
|
|
139
|
+
}
|
|
140
|
+
case "save_voice_profile": {
|
|
141
|
+
const { voice_text } = args;
|
|
142
|
+
await storage.saveVoiceProfile(voice_text);
|
|
143
|
+
return textResult("Voice profile saved.");
|
|
144
|
+
}
|
|
145
|
+
case "get_voice_profile": {
|
|
146
|
+
const voice = await storage.getVoiceProfile();
|
|
147
|
+
return textResult(voice ?? "No voice profile stored yet.");
|
|
148
|
+
}
|
|
149
|
+
case "save_jd": {
|
|
150
|
+
const { company, role, jd_text, jd_path } = args;
|
|
151
|
+
const text = jd_text ?? (jd_path ? await storage.readTextFile(jd_path) : undefined);
|
|
152
|
+
if (!text)
|
|
153
|
+
throw new Error("Provide either jd_text or jd_path.");
|
|
154
|
+
const application = await storage.saveJD(company, role, text);
|
|
155
|
+
return textResult(`Saved job description for ${company} — ${role}. Set as the active application (slug: ${application.slug}).`);
|
|
156
|
+
}
|
|
157
|
+
case "get_active_application": {
|
|
158
|
+
const application = await storage.getActiveApplication();
|
|
159
|
+
if (!application)
|
|
160
|
+
return textResult("No active application. Use save_jd first.");
|
|
161
|
+
const [resume, voice, stories] = await Promise.all([
|
|
162
|
+
storage.getResume(),
|
|
163
|
+
storage.getVoiceProfile(),
|
|
164
|
+
storage.queryStories(),
|
|
165
|
+
]);
|
|
166
|
+
const relevantStories = stories.filter((s) => application.jdText.toLowerCase().split(/\W+/).some((word) => word.length > 4 && s.content.toLowerCase().includes(word)));
|
|
167
|
+
const sections = [
|
|
168
|
+
`## Active application: ${application.company} — ${application.role}`,
|
|
169
|
+
"",
|
|
170
|
+
"### Job description",
|
|
171
|
+
application.jdText,
|
|
172
|
+
"",
|
|
173
|
+
"### Resume",
|
|
174
|
+
resume?.rawText ? resume.rawText : "(none stored)",
|
|
175
|
+
"",
|
|
176
|
+
"### Voice profile",
|
|
177
|
+
voice ?? "(none stored — write in a clear, direct, honest tone; avoid buzzwords and AI-sounding phrases)",
|
|
178
|
+
];
|
|
179
|
+
if (relevantStories.length > 0) {
|
|
180
|
+
sections.push("", "### Potentially relevant stories", ...relevantStories.map((s) => `**${s.title}** [${s.tags.join(", ")}]\n${s.content}`));
|
|
181
|
+
}
|
|
182
|
+
return textResult(sections.join("\n"));
|
|
183
|
+
}
|
|
184
|
+
case "list_applications": {
|
|
185
|
+
const applications = await storage.listApplications();
|
|
186
|
+
if (applications.length === 0)
|
|
187
|
+
return textResult("No applications stored yet.");
|
|
188
|
+
return textResult(applications.map((a) => `- ${a.company} — ${a.role} (${a.slug}, created ${a.createdAt})`).join("\n"));
|
|
189
|
+
}
|
|
190
|
+
case "save_output": {
|
|
191
|
+
const { output_type, content, application_slug } = args;
|
|
192
|
+
let slug = application_slug;
|
|
193
|
+
if (!slug) {
|
|
194
|
+
const active = await storage.getActiveApplication();
|
|
195
|
+
if (!active)
|
|
196
|
+
throw new Error("No active application and no application_slug provided.");
|
|
197
|
+
slug = active.slug;
|
|
198
|
+
}
|
|
199
|
+
const filePath = await storage.saveOutput(slug, output_type, content);
|
|
200
|
+
return textResult(`Saved ${output_type} to ${filePath}`);
|
|
201
|
+
}
|
|
202
|
+
case "save_story": {
|
|
203
|
+
const { title, tags, content } = args;
|
|
204
|
+
const story = await storage.saveStory(title, tags, content);
|
|
205
|
+
return textResult(`Story saved: ${story.title} (${story.slug})`);
|
|
206
|
+
}
|
|
207
|
+
case "query_stories": {
|
|
208
|
+
const { tags } = args;
|
|
209
|
+
const stories = await storage.queryStories(tags);
|
|
210
|
+
if (stories.length === 0)
|
|
211
|
+
return textResult("No matching stories found.");
|
|
212
|
+
return textResult(stories.map((s) => `**${s.title}** [${s.tags.join(", ")}]\n${s.content}`).join("\n\n---\n\n"));
|
|
213
|
+
}
|
|
214
|
+
case "start_story_interview": {
|
|
215
|
+
const step = startStoryInterview();
|
|
216
|
+
return textResult(`Question 1 of ${step.total_questions}:\n${step.question}\n\n(session_id: ${step.session_id})`);
|
|
217
|
+
}
|
|
218
|
+
case "answer_story_question": {
|
|
219
|
+
const { session_id, answer } = args;
|
|
220
|
+
const step = await answerStoryQuestion(session_id, answer);
|
|
221
|
+
if (step.status === "complete") {
|
|
222
|
+
return textResult(`Story complete and saved: "${step.story?.title}" (tags: ${step.story?.tags.join(", ")})`);
|
|
223
|
+
}
|
|
224
|
+
return textResult(`Question ${step.question_number} of ${step.total_questions}:\n${step.question}\n\n(session_id: ${step.session_id})`);
|
|
225
|
+
}
|
|
226
|
+
default:
|
|
227
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
232
|
+
return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
function textResult(text) {
|
|
236
|
+
return { content: [{ type: "text", text }] };
|
|
237
|
+
}
|
|
238
|
+
async function main() {
|
|
239
|
+
const transport = new StdioServerTransport();
|
|
240
|
+
await server.connect(transport);
|
|
241
|
+
}
|
|
242
|
+
main().catch((err) => {
|
|
243
|
+
console.error("career-mcp server error:", err);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { saveStory } from "./storage.js";
|
|
3
|
+
const QUESTIONS = [
|
|
4
|
+
{
|
|
5
|
+
key: "scene",
|
|
6
|
+
prompt: "What's a specific moment you're thinking of? Set the scene — your role, the team, roughly when this was.",
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
key: "situation",
|
|
10
|
+
prompt: "What was the problem or goal, and why did it actually matter?",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
key: "action",
|
|
14
|
+
prompt: "What did you specifically do? Walk through your own actions, not the team's.",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
key: "result",
|
|
18
|
+
prompt: "What happened as a result? Include real numbers or concrete outcomes if you have them.",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
key: "reflection",
|
|
22
|
+
prompt: "Looking back, what's the honest tradeoff, or the thing you'd do differently?",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
key: "tags",
|
|
26
|
+
prompt: "What would you tag this story as? (comma-separated, e.g. leadership, conflict, technical-deep-dive, failure-recovery)",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: "title",
|
|
30
|
+
prompt: "Give this story a short title (3-6 words) so you can find it again later.",
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
const sessions = new Map();
|
|
34
|
+
export function startStoryInterview() {
|
|
35
|
+
const session_id = randomUUID();
|
|
36
|
+
sessions.set(session_id, { index: 0, answers: {} });
|
|
37
|
+
return {
|
|
38
|
+
session_id,
|
|
39
|
+
status: "in_progress",
|
|
40
|
+
question: QUESTIONS[0].prompt,
|
|
41
|
+
question_number: 1,
|
|
42
|
+
total_questions: QUESTIONS.length,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export async function answerStoryQuestion(session_id, answer) {
|
|
46
|
+
const session = sessions.get(session_id);
|
|
47
|
+
if (!session) {
|
|
48
|
+
throw new Error(`No interview session found for session_id: ${session_id}. Start a new one with start_story_interview.`);
|
|
49
|
+
}
|
|
50
|
+
const current = QUESTIONS[session.index];
|
|
51
|
+
session.answers[current.key] = answer;
|
|
52
|
+
session.index += 1;
|
|
53
|
+
if (session.index < QUESTIONS.length) {
|
|
54
|
+
const next = QUESTIONS[session.index];
|
|
55
|
+
return {
|
|
56
|
+
session_id,
|
|
57
|
+
status: "in_progress",
|
|
58
|
+
question: next.prompt,
|
|
59
|
+
question_number: session.index + 1,
|
|
60
|
+
total_questions: QUESTIONS.length,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const { scene, situation, action, result, reflection, tags, title } = session.answers;
|
|
64
|
+
const content = [
|
|
65
|
+
`**Scene:** ${scene}`,
|
|
66
|
+
"",
|
|
67
|
+
`**Situation:** ${situation}`,
|
|
68
|
+
"",
|
|
69
|
+
`**Action:** ${action}`,
|
|
70
|
+
"",
|
|
71
|
+
`**Result:** ${result}`,
|
|
72
|
+
"",
|
|
73
|
+
`**Reflection:** ${reflection}`,
|
|
74
|
+
].join("\n");
|
|
75
|
+
const storyTags = tags.split(",").map((t) => t.trim()).filter(Boolean);
|
|
76
|
+
const story = await saveStory(title, storyTags, content);
|
|
77
|
+
sessions.delete(session_id);
|
|
78
|
+
return { session_id, status: "complete", story };
|
|
79
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Only used by the standalone CLI (src/cli.ts). The MCP server itself never
|
|
2
|
+
// calls an LLM — it relies on the host (Claude Code/Desktop, etc.) to reason
|
|
3
|
+
// over the data these tools return. This file exists for people running
|
|
4
|
+
// career-mcp without an MCP-capable client. It always uses YOUR OWN API key —
|
|
5
|
+
// career-mcp never calls a shared/hosted key, so there's no cost or liability
|
|
6
|
+
// on the maintainer's side.
|
|
7
|
+
function getProvider() {
|
|
8
|
+
const provider = (process.env.CAREER_MCP_LLM_PROVIDER || "groq").toLowerCase();
|
|
9
|
+
if (provider !== "groq" && provider !== "gemini") {
|
|
10
|
+
throw new Error(`Unknown CAREER_MCP_LLM_PROVIDER: ${provider}. Use "groq" or "gemini".`);
|
|
11
|
+
}
|
|
12
|
+
return provider;
|
|
13
|
+
}
|
|
14
|
+
function getApiKey() {
|
|
15
|
+
const key = process.env.CAREER_MCP_API_KEY;
|
|
16
|
+
if (!key) {
|
|
17
|
+
throw new Error("CAREER_MCP_API_KEY is not set. The standalone CLI needs your own API key " +
|
|
18
|
+
"(a free-tier Groq or Gemini key both work) — see .env.example.");
|
|
19
|
+
}
|
|
20
|
+
return key;
|
|
21
|
+
}
|
|
22
|
+
async function callGroq(systemPrompt, userPrompt) {
|
|
23
|
+
const model = process.env.CAREER_MCP_LLM_MODEL || "llama-3.3-70b-versatile";
|
|
24
|
+
const res = await fetch("https://api.groq.com/openai/v1/chat/completions", {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
Authorization: `Bearer ${getApiKey()}`,
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
model,
|
|
32
|
+
temperature: 0.4,
|
|
33
|
+
messages: [
|
|
34
|
+
{ role: "system", content: systemPrompt },
|
|
35
|
+
{ role: "user", content: userPrompt },
|
|
36
|
+
],
|
|
37
|
+
}),
|
|
38
|
+
});
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
throw new Error(`Groq API error (${res.status}): ${await res.text()}`);
|
|
41
|
+
}
|
|
42
|
+
const data = await res.json();
|
|
43
|
+
return data.choices?.[0]?.message?.content ?? "";
|
|
44
|
+
}
|
|
45
|
+
async function callGemini(systemPrompt, userPrompt) {
|
|
46
|
+
const model = process.env.CAREER_MCP_LLM_MODEL || "gemini-2.0-flash";
|
|
47
|
+
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${getApiKey()}`, {
|
|
48
|
+
method: "POST",
|
|
49
|
+
headers: { "Content-Type": "application/json" },
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
systemInstruction: { parts: [{ text: systemPrompt }] },
|
|
52
|
+
contents: [{ parts: [{ text: userPrompt }] }],
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
throw new Error(`Gemini API error (${res.status}): ${await res.text()}`);
|
|
57
|
+
}
|
|
58
|
+
const data = await res.json();
|
|
59
|
+
return data.candidates?.[0]?.content?.parts?.[0]?.text ?? "";
|
|
60
|
+
}
|
|
61
|
+
export async function callLLM(systemPrompt, userPrompt) {
|
|
62
|
+
const provider = getProvider();
|
|
63
|
+
return provider === "groq" ? callGroq(systemPrompt, userPrompt) : callGemini(systemPrompt, userPrompt);
|
|
64
|
+
}
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile, readdir, access } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { PDFParse } from "pdf-parse";
|
|
5
|
+
const DATA_DIR = process.env.CAREER_MCP_DATA_DIR || path.join(os.homedir(), ".career-mcp");
|
|
6
|
+
const STORIES_DIR = path.join(DATA_DIR, "stories");
|
|
7
|
+
const APPLICATIONS_DIR = path.join(DATA_DIR, "applications");
|
|
8
|
+
const RESUME_PATH = path.join(DATA_DIR, "resume.json");
|
|
9
|
+
const VOICE_PROFILE_PATH = path.join(DATA_DIR, "voice_profile.md");
|
|
10
|
+
const ACTIVE_APPLICATION_PATH = path.join(DATA_DIR, "active_application.json");
|
|
11
|
+
async function ensureDirs() {
|
|
12
|
+
await mkdir(DATA_DIR, { recursive: true });
|
|
13
|
+
await mkdir(STORIES_DIR, { recursive: true });
|
|
14
|
+
await mkdir(APPLICATIONS_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
async function exists(p) {
|
|
17
|
+
try {
|
|
18
|
+
await access(p);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function slugify(input) {
|
|
26
|
+
return input
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.trim()
|
|
29
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
30
|
+
.replace(/^-+|-+$/g, "");
|
|
31
|
+
}
|
|
32
|
+
export async function readTextFile(filePath) {
|
|
33
|
+
const expanded = filePath.startsWith("~") ? path.join(os.homedir(), filePath.slice(1)) : filePath;
|
|
34
|
+
const resolved = path.resolve(expanded);
|
|
35
|
+
if (resolved.toLowerCase().endsWith(".pdf")) {
|
|
36
|
+
return readPdfText(resolved);
|
|
37
|
+
}
|
|
38
|
+
return readFile(resolved, "utf8");
|
|
39
|
+
}
|
|
40
|
+
async function readPdfText(resolvedPath) {
|
|
41
|
+
const buffer = await readFile(resolvedPath);
|
|
42
|
+
const parser = new PDFParse({ data: buffer });
|
|
43
|
+
try {
|
|
44
|
+
const result = await parser.getText();
|
|
45
|
+
return result.text;
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
await parser.destroy();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export async function saveResume(rawText) {
|
|
52
|
+
await ensureDirs();
|
|
53
|
+
const resume = { rawText, updatedAt: new Date().toISOString() };
|
|
54
|
+
await writeFile(RESUME_PATH, JSON.stringify(resume, null, 2), "utf8");
|
|
55
|
+
return resume;
|
|
56
|
+
}
|
|
57
|
+
export async function getResume() {
|
|
58
|
+
if (!(await exists(RESUME_PATH)))
|
|
59
|
+
return null;
|
|
60
|
+
return JSON.parse(await readFile(RESUME_PATH, "utf8"));
|
|
61
|
+
}
|
|
62
|
+
export async function saveVoiceProfile(text) {
|
|
63
|
+
await ensureDirs();
|
|
64
|
+
await writeFile(VOICE_PROFILE_PATH, text, "utf8");
|
|
65
|
+
}
|
|
66
|
+
export async function getVoiceProfile() {
|
|
67
|
+
if (!(await exists(VOICE_PROFILE_PATH)))
|
|
68
|
+
return null;
|
|
69
|
+
return readFile(VOICE_PROFILE_PATH, "utf8");
|
|
70
|
+
}
|
|
71
|
+
export async function saveStory(title, tags, content) {
|
|
72
|
+
await ensureDirs();
|
|
73
|
+
const slug = `${slugify(title)}-${Date.now().toString(36)}`;
|
|
74
|
+
const story = { slug, title, tags, content, createdAt: new Date().toISOString() };
|
|
75
|
+
const frontmatter = [
|
|
76
|
+
"---",
|
|
77
|
+
`title: ${title}`,
|
|
78
|
+
`tags: ${tags.join(", ")}`,
|
|
79
|
+
`createdAt: ${story.createdAt}`,
|
|
80
|
+
"---",
|
|
81
|
+
"",
|
|
82
|
+
content,
|
|
83
|
+
].join("\n");
|
|
84
|
+
await writeFile(path.join(STORIES_DIR, `${slug}.md`), frontmatter, "utf8");
|
|
85
|
+
return story;
|
|
86
|
+
}
|
|
87
|
+
export async function queryStories(tags) {
|
|
88
|
+
await ensureDirs();
|
|
89
|
+
const files = (await readdir(STORIES_DIR)).filter((f) => f.endsWith(".md"));
|
|
90
|
+
const stories = [];
|
|
91
|
+
for (const file of files) {
|
|
92
|
+
const raw = await readFile(path.join(STORIES_DIR, file), "utf8");
|
|
93
|
+
const match = raw.match(/^---\ntitle: (.*)\ntags: (.*)\ncreatedAt: (.*)\n---\n\n([\s\S]*)$/);
|
|
94
|
+
if (!match)
|
|
95
|
+
continue;
|
|
96
|
+
const [, title, tagsLine, createdAt, content] = match;
|
|
97
|
+
const storyTags = tagsLine.split(",").map((t) => t.trim()).filter(Boolean);
|
|
98
|
+
stories.push({ slug: file.replace(/\.md$/, ""), title, tags: storyTags, content, createdAt });
|
|
99
|
+
}
|
|
100
|
+
if (!tags || tags.length === 0)
|
|
101
|
+
return stories;
|
|
102
|
+
const wanted = tags.map((t) => t.toLowerCase());
|
|
103
|
+
return stories.filter((s) => s.tags.some((t) => wanted.includes(t.toLowerCase())));
|
|
104
|
+
}
|
|
105
|
+
export async function saveJD(company, role, jdText) {
|
|
106
|
+
await ensureDirs();
|
|
107
|
+
const slug = slugify(`${company}-${role}`);
|
|
108
|
+
const appDir = path.join(APPLICATIONS_DIR, slug);
|
|
109
|
+
await mkdir(path.join(appDir, "outputs"), { recursive: true });
|
|
110
|
+
const application = { slug, company, role, jdText, createdAt: new Date().toISOString() };
|
|
111
|
+
await writeFile(path.join(appDir, "jd.json"), JSON.stringify(application, null, 2), "utf8");
|
|
112
|
+
await writeFile(ACTIVE_APPLICATION_PATH, JSON.stringify({ slug }, null, 2), "utf8");
|
|
113
|
+
return application;
|
|
114
|
+
}
|
|
115
|
+
export async function getApplication(slug) {
|
|
116
|
+
const p = path.join(APPLICATIONS_DIR, slug, "jd.json");
|
|
117
|
+
if (!(await exists(p)))
|
|
118
|
+
return null;
|
|
119
|
+
return JSON.parse(await readFile(p, "utf8"));
|
|
120
|
+
}
|
|
121
|
+
export async function getActiveApplication() {
|
|
122
|
+
if (!(await exists(ACTIVE_APPLICATION_PATH)))
|
|
123
|
+
return null;
|
|
124
|
+
const { slug } = JSON.parse(await readFile(ACTIVE_APPLICATION_PATH, "utf8"));
|
|
125
|
+
return getApplication(slug);
|
|
126
|
+
}
|
|
127
|
+
export async function listApplications() {
|
|
128
|
+
await ensureDirs();
|
|
129
|
+
const dirs = await readdir(APPLICATIONS_DIR);
|
|
130
|
+
const applications = [];
|
|
131
|
+
for (const dir of dirs) {
|
|
132
|
+
const application = await getApplication(dir);
|
|
133
|
+
if (application)
|
|
134
|
+
applications.push(application);
|
|
135
|
+
}
|
|
136
|
+
return applications.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
137
|
+
}
|
|
138
|
+
export async function saveOutput(slug, type, content) {
|
|
139
|
+
const appDir = path.join(APPLICATIONS_DIR, slug, "outputs");
|
|
140
|
+
await mkdir(appDir, { recursive: true });
|
|
141
|
+
const filePath = path.join(appDir, `${type}.md`);
|
|
142
|
+
await writeFile(filePath, content, "utf8");
|
|
143
|
+
return filePath;
|
|
144
|
+
}
|
|
145
|
+
export async function getOutput(slug, type) {
|
|
146
|
+
const filePath = path.join(APPLICATIONS_DIR, slug, "outputs", `${type}.md`);
|
|
147
|
+
if (!(await exists(filePath)))
|
|
148
|
+
return null;
|
|
149
|
+
return readFile(filePath, "utf8");
|
|
150
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sindhujaks/career-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for resume/JD match analysis, tailored resumes and cover letters, a structured career-story interview, and LinkedIn About drafting. Local-first — your data never leaves your machine.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"career-mcp": "dist/index.js",
|
|
9
|
+
"career-mcp-cli": "dist/cli.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
".env.example",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"start": "node dist/index.js",
|
|
23
|
+
"dev": "node --loader ts-node/esm src/index.ts",
|
|
24
|
+
"cli": "node dist/cli.js",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"mcp",
|
|
29
|
+
"model-context-protocol",
|
|
30
|
+
"resume",
|
|
31
|
+
"career",
|
|
32
|
+
"job-search",
|
|
33
|
+
"claude",
|
|
34
|
+
"ai-agent"
|
|
35
|
+
],
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/sindhujaIBM/career-mcp.git"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/sindhujaIBM/career-mcp#readme",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/sindhujaIBM/career-mcp/issues"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
49
|
+
"dotenv": "^16.4.0",
|
|
50
|
+
"pdf-parse": "^2.4.5"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^22.0.0",
|
|
54
|
+
"ts-node": "^10.9.2",
|
|
55
|
+
"typescript": "^5.5.0"
|
|
56
|
+
},
|
|
57
|
+
"license": "MIT"
|
|
58
|
+
}
|