Skip to main content
Glama
edefice

job-search-mcp

by edefice
README.md
# Job Search MCP Server

An MCP (Model Context Protocol) server that lets Claude search [justjoin.it](https://justjoin.it)
job postings and track your applications, so you can ask things like *"find me new AI Engineer
roles in Warsaw"* or *"what's the status of the application I sent last week"* directly in a chat with
Claude Desktop or Claude Code.

## Why

Built as a hands-on MCP project for my own AI Engineer job search β€” real tool, real use, not a
toy demo.

## Tools

| Tool | Description |
|---|---|
| `search_jobs(keyword, location?)` | Searches justjoin.it listings. Returns title, company, URL, salary, tags, location. `location` accepts a city name (`"Warsaw"`, `"Krakow"`, ...) or `"remote"`. |
| `get_job_details(url)` | Fetches a single job offer page and returns a cleaned-up description plus tech stack tags. |
| `track_application(url, status)` | Saves/updates an application's status locally. `status` is one of `saved`, `applied`, `interviewing`, `rejected`, `offer`. |
| `list_applications()` | Lists all tracked applications, most recently updated first. |

Application data is stored locally in a JSON file at `~/.job-search-mcp/applications.json` β€”
no database, no external service.

## Setup

Requires Node.js 18+.

```bash
git clone <this-repo-url>
cd job-search-mcp
npm install
npm run build
```

This produces `dist/index.js`, a stdio MCP server.

## Connecting to Claude Desktop

Add the server to your Claude Desktop config
(`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS), replacing the
path below with the absolute path to `dist/index.js` in your clone (run `pwd` inside the
project directory to get it):

```json
{
  "mcpServers": {
    "job-search": {
      "command": "node",
      "args": ["/absolute/path/to/job-search-mcp/dist/index.js"]
    }
  }
}
```

Restart Claude Desktop. The four tools above should appear as available tools (look for the
πŸ”¨ tools icon in the chat input).

## Connecting to Claude Code

```bash
claude mcp add job-search -- node /absolute/path/to/job-search-mcp/dist/index.js
```

Or add it to a project's `.mcp.json`:

```json
{
  "mcpServers": {
    "job-search": {
      "command": "node",
      "args": ["/absolute/path/to/job-search-mcp/dist/index.js"]
    }
  }
}
```

## Example prompts

- "Find AI Engineer roles in Warsaw"
- "Get me the full details and tech stack for this listing: `<url>`"
- "Mark that job as applied"
- "What's the status of all my job applications?"
- "Show me everything I've marked as interviewing"

## Development

```bash
npm run dev    # tsc --watch
npm run build  # one-off compile
npm start      # run the compiled server directly (for manual stdio testing)
npm test       # build, then run the test suite (node's built-in test runner)
```

## Tests

Tests run with Node's built-in test runner (`node:test`) β€” no extra test framework dependency.
`npm test` compiles first, then runs everything under `dist/**/*.test.js`.

- `justjoin.test.ts` β€” `buildSearchUrl` against known/unmapped/diacritic city names, and
  `parseOfferCards`/`parseJobDetails` against trimmed-but-verbatim fixtures of real justjoin.it
  markup (including the `<object>` logo wrapper that makes the tag-pill parsing heuristic
  necessary), so the parsing logic is verified without hitting the live site.
- `store.test.ts` β€” `trackApplication`/`listApplications` against a temp file (via the
  `JOB_SEARCH_STORE_PATH` env var override), so tests never touch your real
  `~/.job-search-mcp/applications.json`.

## Tricky bits

- **justjoin.it has no public API.** It's a Next.js app that server-renders offer cards, but
  the CSS classes are build-hashed (e.g. `mui-1cks7or`) and change on every deploy, so scraping
  can't rely on fixed selectors. `src/justjoin.ts` anchors on stable landmarks instead: the
  `offer_list_offer_title_link` class, Lucide icon classes (`svg.lucide-map-pin`,
  `svg.lucide-building`), and β€” for the tag pills, where even sibling/depth assumptions turned
  out to be unreliable β€” a structural heuristic that detects "a `<div>` whose children are all
  plain-text, childless `<div>`s," a shape unique to that row on the card.
- **Job detail pages are easier**: they embed proper `schema.org/JobPosting` JSON-LD, which is
  used for title/company/location/salary/employment type. The visible HTML is still used for
  the description and tech stack, since the JSON-LD description is a single run-on string with
  no paragraph breaks.
- **City slugs are Polish-only** (`warszawa`, not `warsaw`) β€” `search_jobs` maps common English
  city names to their Polish slugs. Writing the test for this caught a real bug: Polish `Ε‚`
  (as in `Łódź`) isn't a combining-mark diacritic, so `String.normalize("NFD")` doesn't strip
  it the way it does `Γ³` or `ΕΊ` β€” it needs an explicit `Ε‚`/`Ł` β†’ `l`/`L` replacement first.
- **Salary is often undisclosed.** Both tools fall back to `"Not specified"` rather than
  erroring when a listing doesn't list a salary.
- **Scraping is inherently brittle.** If justjoin.it changes its page structure enough to break
  these heuristics, `search_jobs`/`get_job_details` will return an explicit tool error rather
  than silently returning garbage.

## Roadmap (not yet built)

- `match_score(job_url)` β€” score a job description against a CV/skills profile via the
  Anthropic API, with a rationale.
- A second job board as a data source, so results aren't tied to justjoin.it alone.

TDQS

B3.4/5.0

Scored across 2 tools

Disambiguation5/5

The two tools have clearly distinct purposes: one fetches job details, the other tracks applications. No overlap or confusion.

Naming Consistency5/5

Both tool names follow a consistent verb_noun pattern: 'get_job_details' and 'track_application'.

Tool Count2/5

With only 2 tools for a 'job-search-mcp', the scope is far too narrow. A job search server typically needs search, list, and filter capabilities.

Completeness1/5

The tool surface is severely incomplete for a job search server. There is no way to discover or search for jobs, only view details of a known posting and track application status.

Maintenance

ActivityInactive
ResponsivenessNo issues