@joana412/job-tracker-mcp 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/README.md +50 -0
- package/package.json +26 -0
- package/server.js +142 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# job-tracker-mcp
|
|
2
|
+
|
|
3
|
+
A small MCP server that lets an AI agent (Claude Code, Claude Desktop,
|
|
4
|
+
Cursor, etc.) keep track of my job applications.
|
|
5
|
+
|
|
6
|
+
## Why I built it
|
|
7
|
+
|
|
8
|
+
I was applying to a lot of jobs and tracking them in my head and in a
|
|
9
|
+
messy spreadsheet. I was already pasting job postings into Claude to
|
|
10
|
+
tailor my resume, so I wanted Claude to also remember what I applied to,
|
|
11
|
+
update the status when I heard back, and tell me which applications
|
|
12
|
+
needed a follow-up.
|
|
13
|
+
|
|
14
|
+
A chat alone can't do that: it forgets everything between conversations,
|
|
15
|
+
and it is unreliable at date maths ("was that 9 days ago or 12?"). This
|
|
16
|
+
server gives the agent a small, permanent store and does the date
|
|
17
|
+
calculation for it.
|
|
18
|
+
|
|
19
|
+
## Tools
|
|
20
|
+
|
|
21
|
+
| Tool | What it does |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `add_application` | Save a job: company, role, location, link, status, notes. |
|
|
24
|
+
| `list_applications` | Show saved jobs, newest first, with `daysSinceUpdate`. Filter by status or company. |
|
|
25
|
+
| `update_application` | Change the status (e.g. `interviewing`, `rejected`) and add dated notes. |
|
|
26
|
+
|
|
27
|
+
Statuses: `saved`, `applied`, `interviewing`, `offer`, `rejected`, `withdrawn`.
|
|
28
|
+
|
|
29
|
+
Data is stored in `~/.job-applications.json`. Set `JOB_TRACKER_FILE` to use
|
|
30
|
+
a different file.
|
|
31
|
+
|
|
32
|
+
## Setup
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git clone https://github.com/joanapie/job-tracker-mcp.git
|
|
36
|
+
cd job-tracker-mcp
|
|
37
|
+
npm install
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Add it to Claude Code:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
claude mcp add job-tracker -- node /absolute/path/to/job-tracker-mcp/server.js
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Things you can then say:
|
|
47
|
+
|
|
48
|
+
- *"Here's a posting I just applied to: [paste]. Add it."*
|
|
49
|
+
- *"Which applications have had no update for more than 10 days?"*
|
|
50
|
+
- *"Konrad invited me to a phone screen on Friday."*
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@joana412/job-tracker-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server that lets an AI agent track my job applications",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/joanapie/job-tracker-mcp"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test test/*.test.js",
|
|
11
|
+
"start": "node server.js"
|
|
12
|
+
},
|
|
13
|
+
"files": ["server.js", "README.md"],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
18
|
+
"zod": "^4.6.5"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20"
|
|
22
|
+
},
|
|
23
|
+
"bin": {
|
|
24
|
+
"job-tracker-mcp": "server.js"
|
|
25
|
+
}
|
|
26
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// MCP server that lets an AI agent keep track of my job applications.
|
|
3
|
+
// Data is stored in one JSON file on my computer.
|
|
4
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
|
|
11
|
+
// Where the data lives. Can be changed with an environment variable.
|
|
12
|
+
const DATA_FILE = process.env.JOB_TRACKER_FILE || join(homedir(), '.job-applications.json');
|
|
13
|
+
|
|
14
|
+
const STATUSES = ['saved', 'applied', 'interviewing', 'offer', 'rejected', 'withdrawn'];
|
|
15
|
+
|
|
16
|
+
// ---------- Reading and saving the file ----------
|
|
17
|
+
|
|
18
|
+
async function load() {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(await readFile(DATA_FILE, 'utf8'));
|
|
21
|
+
} catch (err) {
|
|
22
|
+
if (err.code === 'ENOENT') return []; // first run: no file yet
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function save(applications) {
|
|
28
|
+
await writeFile(DATA_FILE, JSON.stringify(applications, null, 2));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const today = () => new Date().toISOString().slice(0, 10); // e.g. 2026-09-22
|
|
32
|
+
|
|
33
|
+
// Days between a date and today, so the agent never has to do date maths.
|
|
34
|
+
const daysSince = (date) => Math.floor((Date.now() - new Date(date).getTime()) / 86_400_000);
|
|
35
|
+
|
|
36
|
+
const reply = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
37
|
+
const fail = (message) => ({ isError: true, content: [{ type: 'text', text: message }] });
|
|
38
|
+
|
|
39
|
+
// ---------- The server and its tools ----------
|
|
40
|
+
|
|
41
|
+
const server = new McpServer({ name: 'job-tracker', version: '1.0.0' });
|
|
42
|
+
|
|
43
|
+
server.registerTool(
|
|
44
|
+
'add_application',
|
|
45
|
+
{
|
|
46
|
+
title: 'Add a job application',
|
|
47
|
+
description:
|
|
48
|
+
'Record a job the user has applied to or wants to apply to. ' +
|
|
49
|
+
'If the user pastes a job posting, extract the company, role and location from it. ' +
|
|
50
|
+
'Call list_applications first to avoid adding the same job twice.',
|
|
51
|
+
inputSchema: {
|
|
52
|
+
company: z.string().describe('Company name'),
|
|
53
|
+
role: z.string().describe('Job title'),
|
|
54
|
+
location: z.string().optional().describe('City, or "Remote"'),
|
|
55
|
+
url: z.string().optional().describe('Link to the posting'),
|
|
56
|
+
status: z.enum(STATUSES).optional().describe('Defaults to "applied"'),
|
|
57
|
+
notes: z.string().optional().describe('Anything worth remembering, e.g. salary range or contact name'),
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
async (input) => {
|
|
61
|
+
const applications = await load();
|
|
62
|
+
const application = {
|
|
63
|
+
id: applications.reduce((max, a) => Math.max(max, a.id), 0) + 1,
|
|
64
|
+
company: input.company,
|
|
65
|
+
role: input.role,
|
|
66
|
+
location: input.location ?? '',
|
|
67
|
+
url: input.url ?? '',
|
|
68
|
+
status: input.status ?? 'applied',
|
|
69
|
+
notes: input.notes ?? '',
|
|
70
|
+
dateAdded: today(),
|
|
71
|
+
lastUpdated: today(),
|
|
72
|
+
};
|
|
73
|
+
applications.push(application);
|
|
74
|
+
await save(applications);
|
|
75
|
+
return reply({ added: application });
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
server.registerTool(
|
|
80
|
+
'list_applications',
|
|
81
|
+
{
|
|
82
|
+
title: 'List job applications',
|
|
83
|
+
description:
|
|
84
|
+
'Show saved job applications, newest first. Each one includes daysSinceUpdate, ' +
|
|
85
|
+
'so you can answer questions like "which applications have had no reply for two weeks?". ' +
|
|
86
|
+
'Optionally filter by status or by company name.',
|
|
87
|
+
inputSchema: {
|
|
88
|
+
status: z.enum(STATUSES).optional(),
|
|
89
|
+
company: z.string().optional().describe('Part of a company name, case-insensitive'),
|
|
90
|
+
},
|
|
91
|
+
annotations: { readOnlyHint: true },
|
|
92
|
+
},
|
|
93
|
+
async ({ status, company }) => {
|
|
94
|
+
let applications = await load();
|
|
95
|
+
if (status) applications = applications.filter((a) => a.status === status);
|
|
96
|
+
if (company) {
|
|
97
|
+
applications = applications.filter((a) => a.company.toLowerCase().includes(company.toLowerCase()));
|
|
98
|
+
}
|
|
99
|
+
const result = applications
|
|
100
|
+
.map((a) => ({ ...a, daysSinceUpdate: daysSince(a.lastUpdated) }))
|
|
101
|
+
.sort((a, b) => b.id - a.id);
|
|
102
|
+
return reply({ count: result.length, applications: result });
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
server.registerTool(
|
|
107
|
+
'update_application',
|
|
108
|
+
{
|
|
109
|
+
title: 'Update a job application',
|
|
110
|
+
description:
|
|
111
|
+
'Change the status of an application or add a note, e.g. when the user gets an interview ' +
|
|
112
|
+
'or a rejection. Can also correct the company, role, location or link. ' +
|
|
113
|
+
'Use the id from list_applications. New notes are added to existing ones.',
|
|
114
|
+
inputSchema: {
|
|
115
|
+
id: z.number().int().describe('The application id'),
|
|
116
|
+
status: z.enum(STATUSES).optional(),
|
|
117
|
+
note: z.string().optional().describe('Added to the existing notes with today\'s date'),
|
|
118
|
+
company: z.string().optional().describe('New company name'),
|
|
119
|
+
role: z.string().optional().describe('New job title'),
|
|
120
|
+
location: z.string().optional().describe('New location: city, or "Remote"'),
|
|
121
|
+
url: z.string().optional().describe('New link to the posting; an empty string clears it'),
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
async ({ id, status, note, company, role, location, url }) => {
|
|
125
|
+
const applications = await load();
|
|
126
|
+
const application = applications.find((a) => a.id === id);
|
|
127
|
+
if (!application) {
|
|
128
|
+
return fail(`No application with id ${id}. Call list_applications to see valid ids.`);
|
|
129
|
+
}
|
|
130
|
+
if (status) application.status = status;
|
|
131
|
+
if (company) application.company = company;
|
|
132
|
+
if (role) application.role = role;
|
|
133
|
+
if (location !== undefined) application.location = location;
|
|
134
|
+
if (url !== undefined) application.url = url;
|
|
135
|
+
if (note) application.notes = [application.notes, `[${today()}] ${note}`].filter(Boolean).join('\n');
|
|
136
|
+
application.lastUpdated = today();
|
|
137
|
+
await save(applications);
|
|
138
|
+
return reply({ updated: application });
|
|
139
|
+
},
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
await server.connect(new StdioServerTransport());
|