Skip to main content
Glama
kaidashova

Job Assistant MCP

by kaidashova
README.md
# Job Assistant MCP

An [MCP](https://modelcontextprotocol.io) server that turns Claude into a job-search assistant.
It pulls **real postings from public job boards**, scores them against **your own stored profile**
(skills, experience, projects) and tracks your applications in **SQLite**.

> *"Find remote Python + AI jobs posted this week and tell me which three fit me best."*

## What it does

| Tool | Purpose |
|---|---|
| `set_profile` / `get_profile` | Store your skills, years of experience, target roles, summary and projects (merged, so you can update one field at a time). |
| `search_jobs` | Query all boards **concurrently**, filter by keywords / remote / recency, dedupe, cache. If a profile exists every result gets a 0-100 fit score and the list is sorted best-first. |
| `match_job_to_profile` | Explainable breakdown for one posting: matched & missing skills, seniority fit, which of *your projects* to mention. |
| `get_job` | Full posting text + your application status. |
| `save_job`, `update_status`, `list_applications` | Application tracker (`saved → applied → screening → interview → offer / rejected / withdrawn`) with full status history and per-stage counts. |

Also exposed: resources `profile://me` and `jobs://pipeline`, and prompts `find_best_jobs` and
`prepare_application` (tailored cover letter + talking points).

### Job sources

| Source | Access | Notes |
|---|---|---|
| [DOU](https://jobs.dou.ua) | official public RSS | Ukrainian IT jobs |
| [Djinni](https://djinni.co) | official public RSS | Ukrainian / European IT jobs |

The scope is intentionally limited to these two Ukrainian IT job boards. LinkedIn is not
supported: it has no public job API and scraping it violates its Terms of Service. New sources are
easy to add, see [Adding a source](#adding-a-source).

A failing board never breaks a search; its error is reported in `errors` and the rest still return.

## Quick start

You need [Docker](https://www.docker.com/products/docker-desktop/) and an MCP client such as
[Claude Code](https://docs.claude.com/en/docs/claude-code).

**1. Start the server**

```bash
git clone https://github.com/kaidashova/job-assistant-mcp.git && cd job-assistant-mcp
docker compose up -d --build
```

The MCP endpoint is now at `http://localhost:8000/mcp`. If port 8000 is taken, pick another one:
`HOST_PORT=8765 docker compose up -d --build` (use the same port in step 2).

**2. Connect your MCP client**

```bash
claude mcp add --transport http job-assistant http://localhost:8000/mcp
claude mcp list        # job-assistant should show as connected
```

Then start `claude` and type `/mcp` to see the 8 tools. Any other MCP client that supports
Streamable HTTP can use the same URL.

**3. Day-to-day**

```bash
docker compose logs -f     # watch the server
docker compose down        # stop (your data is kept)
docker compose down -v     # stop and delete all data (profile, applications)
```

The container restarts automatically with Docker. Your profile and applications live in the
`job-data` Docker volume, so they survive restarts and rebuilds.

The port is bound to `127.0.0.1` only. There is no authentication, so don't expose it publicly
without a reverse proxy in front.

## Try it

1. Tell Claude about yourself once:
   > *Save my profile: skills Python, FastAPI, PostgreSQL, Docker, LLM, RAG, MCP; 3 years of
   > experience; I want Python Developer or AI Engineer roles. Projects: "Job Assistant MCP"
   > (MCP server, tech: python, sqlite, mcp), "Docs Q&A bot" (tech: python, rag, postgresql).*
2. Use the **`find_best_jobs`** prompt, or just ask:
   > *Find remote Python + AI jobs posted this week and tell me which three fit me best.*
3. > *Save the first one and mark it as applied.* → later: *"What's in my pipeline?"*

## How scoring works

Deterministic and explainable, so Claude can reason over the parts instead of a black box:

`score = 65 × skill coverage + 20 × role fit + 15 × seniority fit`

* **Skill coverage**: skills are extracted from the posting using a vocabulary of ~90 technologies
  with aliases (`k8s → kubernetes`, `postgres → postgresql`; ambiguous words like *go* only match as
  `golang`). Skills in the title/tags count double. Umbrella skills are implied (LLM/RAG ⇒ AI,
  PostgreSQL ⇒ SQL). Skills outside the vocabulary that you list in your profile are still searched for.
* **Role fit**: word overlap between your target roles and the job title.
* **Seniority fit**: level from the title (junior … lead) vs. your years of experience.

## Architecture

Layered, with dependencies pointing downward only:

```
server/         MCP adapters: tools, resources, prompts (validate input -> call service -> typed output)
   ↓
services/       business logic: SearchService, TrackerService, ProfileService, MatchingService
   ↓
repositories/   SQLite access only: Database, JobRepository, ApplicationRepository, ProfileRepository
sources/        one module per job board, all behind the same JobSource protocol
matching/       pure scoring engine + skill extraction (no I/O)
utils/          small stateless helpers (HTML to text, date parsing)
schemas/        Pydantic models shared by every layer; tool results are typed, so the server
                publishes JSON output schemas for each tool
constants/      all module-level constants (skill vocabulary, scoring weights, feed URLs, SQL schema…)
config.py       Settings (pydantic BaseSettings, read from env / .env)
container.py    composition root: builds the repositories and services and wires them together
errors.py       JobAssistantError, surfaced to the model as a clean tool error
```

```
app/
  config.py  container.py  errors.py
  constants/      skills, matching, sources, search, database, server
  schemas/        job, application, profile, search, matching, source
  repositories/   database, jobs, applications, profile
  services/       search, tracker, profile, matching, filters
  matching/       scoring, skills
  sources/        base, rss, dou, djinni
  utils/          text (HTML cleanup, remote detection), dates
  server/         app, tools, resources, prompts
tests/            offline: mocked HTTP + fixtures, one test module per layer, plus end-to-end over MCP
```

Services never import MCP and the server never touches SQL, so each layer is testable on its own.

## Development

```bash
make install   # local .venv with dev tools (uv)
make test      # offline tests, no network needed
make lint      # ruff + mypy
```

CI (GitHub Actions) runs lint + tests on Python 3.11-3.13 and builds the Docker image.

### Adding a source

Create `app/sources/mysource.py` with a class that has a `name` and an
`async fetch(client, query) -> list[Job]`, then register it in `sources/__init__.py`. Filtering by
keyword / remote / date is done centrally in `services/filters.py`, so a source only needs to fetch and map
fields. Add a fixture and a test in `tests/`.

## Configuration

| Variable | Default | Meaning |
|---|---|---|
| `JOB_ASSISTANT_DB` | `/data/jobs.db` in Docker | SQLite file |
| `MCP_TRANSPORT` | `streamable-http` in `docker-compose.yml` | `stdio` or `streamable-http` |
| `MCP_HOST` / `MCP_PORT` | `0.0.0.0` / `8000` in Docker | HTTP bind address inside the container |
| `JOB_ASSISTANT_HTTP_TIMEOUT` | `20` | Seconds before a job-board request times out |

To change one, add it under `environment:` in `docker-compose.yml` and run `docker compose up -d` again.

## Notes

* Built on the official MCP Python SDK (v2, `MCPServer`).
* Be a good citizen: only public feeds/APIs are used, with a descriptive User-Agent and one request
  per board per search.
* Your profile and applications never leave your machine; only the search keywords go to the boards.

MIT licensed.

TDQS

A3.7/5.0

Scored across 8 tools

Disambiguation5/5

Each tool targets a distinct resource and action: profile (get/set), jobs (search/get/save/match), and applications (update_status/list_applications). The fit-scoring overlap between search_jobs and match_job_to_profile is intentional and clearly separated by scope. An agent can reliably pick the right tool for any task.

Naming Consistency5/5

All tools use a consistent snake_case verb_noun pattern (get_profile, search_jobs, save_job, list_applications, set_profile, update_status). Naming is predictable and self-describing throughout.

Tool Count5/5

8 tools is well-scoped for a job-assistant domain covering profile management, job discovery, matching, and application tracking. No redundant or filler tools; each earns its place.

Completeness4/5

Covers the core lifecycle: profile get/set, job search/get/match/save, and application tracking with status updates and counts. Minor gaps exist (no way to remove/unsave a job or delete a profile), but agents can work around them.

Maintenance

ActivityMaintained
ResponsivenessNo issues