CareerPilot
# CareerPilot ๐งญ

An AI **job-hunt copilot** built on the [Model Context Protocol](https://modelcontextprotocol.io) โ
search live job boards, shortlist openings, track your application pipeline, and generate
tailored resumes/cover letters, all from any MCP client (Claude Desktop, Claude Code, MCP Inspector).
Built as a learning project that deliberately exercises **every major MCP concept** in a real product.
## Real data, no API keys
| Source | What it is |
|---|---|
| [Remotive](https://remotive.com) | Remote job board, free public API |
| [RemoteOK](https://remoteok.com) | Remote job board, free public API |
| Hacker News "Who is hiring?" | Monthly hiring thread, via the free Algolia API |
Your shortlist, applications, and profile live in a local SQLite DB (`~/.careerpilot/careerpilot.db`).
## MCP feature map
| MCP concept | Where it lives in this project | What it teaches |
|---|---|---|
| **Tools** | `search_jobs`, `save_job`, `track_application`, `update_application`, `schedule_follow_up`, `set_profile` | Model-callable actions with typed schemas |
| **Resources** | `careerpilot://pipeline`, `careerpilot://saved-jobs`, `careerpilot://profile` | App data exposed as readable context |
| **Resource templates** | `careerpilot://applications/{app_id}` | Parameterized URIs |
| **Prompts** | `tailor_resume`, `cover_letter`, `interview_prep` | Reusable, server-defined prompt workflows |
| **Sampling** | `score_job_fit` โ the server asks the *client's* LLM to judge fit | Server โ LLM inversion; server needs no API key |
| **Elicitation** | `delete_application` asks the user to confirm | Mid-tool-call user input |
| **Roots** | `find_resume` scans client-granted folders | Filesystem boundaries negotiated with the client |
| **Subscriptions** | pipeline & watches emit `resources/updated` on every change | Push notifications to subscribed clients |
| **Logging & progress** | `ctx.info()` / `ctx.report_progress()` in `search_jobs` | Server โ client observability |
| **Background notifications** | `watch_search` + lifespan poller push updates with *no request in flight* | Server-initiated protocol traffic |
| **Streamable HTTP** | `careerpilot --http` | The production transport |
| **Authorization** | Bearer-token resource server (`auth.py`, 401 + `WWW-Authenticate`) | The MCP auth spec's resource-server side |
| **The client side** | `careerpilot-chat` (`host.py`) โ a full MCP host on the Anthropic API | Handshake, tool loop, sampling/elicitation/roots handlers |
## Quickstart
```bash
uv sync
# Interactive protocol playground (best way to learn):
uv run mcp dev src/careerpilot/server.py
# โ opens MCP Inspector in the browser; poke every tool/resource/prompt,
# and test sampling + elicitation from the Inspector UI
```
**Claude Code:** this repo ships a `.mcp.json`, so just open the project and approve the server.
**Claude Desktop:** `claude_desktop_config.json` โ
```json
{
"mcpServers": {
"careerpilot": {
"command": "uv",
"args": ["run", "--directory", "/Users/shwetarani/Developer/MCP_Project", "careerpilot"]
}
}
}
```
Then try, in plain language:
> *"Search for remote python jobs, save the best three, and track that I applied to the first one."*
> *"Read my pipeline and tell me who I should follow up with."*
> *"Use the cover letter prompt for job 2."*
## Architecture
```
โโโโโโโโโโโโโโโโโโโโโโโ MCP (stdio โ later Streamable HTTP)
โ MCP client + LLM โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ (Claude Code, etc.) โ โ
โโโโโโโโโโโโฌโโโโโโโโโโโ โ
โ tools / resources / prompts sampling / elicitation / roots
โผ (server โ client callbacks)
โโโโโโโโโโโโโโโโโโโโโโโ
โ CareerPilot server โ src/careerpilot/server.py (FastMCP)
โ โโโ sources.py โ Remotive ยท RemoteOK ยท HN (httpx, concurrent)
โ โโโ db.py โ SQLite: saved_jobs ยท applications ยท profile
โโโโโโโโโโโโโโโโโโโโโโโ
```
## Watched searches (stage 2)
```
you> watch this search: "python backend", remotive only
-> Watch #1 created ... baseline: 10 current listings
```
A background poller re-runs every watch (default: every 15 min, tune with
`CAREERPILOT_POLL_SECONDS`) and pushes `resources/updated` notifications to any
client subscribed to `careerpilot://watches` โ the server talks first, with no
request in flight. Review new finds in `careerpilot://watches/{id}`, then
`mark_watch_reviewed`.
## Production transport: HTTP + auth (stage 3)
```bash
# Serve over Streamable HTTP with bearer-token auth
CAREERPILOT_TOKEN=$(openssl rand -hex 24) uv run careerpilot --http --port 8848
# MCP endpoint: http://127.0.0.1:8848/mcp (requests without the token get 401)
```
`auth.py` implements the SDK's `TokenVerifier` โ the *resource server* role in
the MCP authorization spec (401 + `WWW-Authenticate`, protected-resource
metadata, scope checks). Swap `StaticTokenVerifier` for a JWT verifier against
a real OAuth 2.1 IdP without touching the rest of the server.
```bash
docker build -t careerpilot .
docker run -p 8848:8848 -v careerpilot-data:/data -e CAREERPILOT_TOKEN=... careerpilot
```
## Web dashboard

`careerpilot --http` also serves a dashboard at `/` โ a "hiring file" view of your
pipeline styled as stamped paperwork: an action tray (follow-ups due, unreviewed watch
finds), the application drawer grouped in triage order, watch index cards, and one-click
"I applied" / "mark reviewed" / status changes. Same process, same SQLite, two front
doors: humans at `/`, LLMs at `/mcp` โ and edits made in the browser push
`resources/updated` notifications to connected MCP clients.
```bash
uv run careerpilot --http # dashboard: http://127.0.0.1:8848/
```
When `CAREERPILOT_TOKEN` is set, the dashboard locks too: open
`/?token=<your token>` once and that browser stays unlocked (cookie);
the JSON API also accepts the same `Authorization: Bearer` header as `/mcp`.
## The client side: your own MCP host (stage 4)
`careerpilot-chat` is a complete MCP **host** in ~250 lines (`src/careerpilot/host.py`) โ
what Claude Desktop does, made visible:
```bash
export ANTHROPIC_API_KEY=sk-ant-...
uv run careerpilot-chat # spawn local server (stdio)
uv run careerpilot-chat --url http://host:8848/mcp --token ... # remote server
uv run careerpilot-chat --list # capability dump (no API key needed)
```
It negotiates capabilities, exposes the server's tools to Claude, runs the
agentic tool-call loop, **answers the server's sampling requests** by calling
the Anthropic API, surfaces elicitation at the terminal, grants roots (cwd),
and prints server logs and push notifications. `/tools`, `/read <uri>`,
`/prompt <name> k=v`, `/quit` inside the REPL.
## Learning roadmap โ complete โ
- [x] **Stage 1 โ Server fundamentals**: tools, resources, templates, prompts,
sampling, elicitation, roots, subscriptions, logging/progress over **stdio**
- [x] **Stage 2 โ Watched searches**: background poller pushes `resources/updated`
notifications when new matching jobs appear
- [x] **Stage 3 โ Production transport**: **Streamable HTTP**, bearer-token auth
(MCP resource-server pattern), Dockerfile
- [x] **Stage 4 โ The client**: `careerpilot-chat`, a minimal MCP **host** on the
Anthropic API โ handshake, tool-call loop, sampling/elicitation/roots handlers
## Development
```bash
uv sync # install
uv run mcp dev src/careerpilot/server.py # inspector
uv run careerpilot # run over stdio directly
CAREERPILOT_DB=/tmp/test.db uv run careerpilot # throwaway database
# End-to-end protocol tests (spawn the real server, no mocks on the MCP layer)
uv run python tests/e2e_stdio_test.py # stage 1: every MCP feature over stdio
uv run python tests/stage2_watches_test.py # stage 2: poller + notifications
uv run python tests/stage3_http_test.py # stage 3: HTTP transport + 401/auth
```
TDQS
Scored across 14 tools
Each tool has a clearly distinct purpose: searching jobs, managing watches, tracking applications, scheduling follow-ups, etc. There is no ambiguity between tools like 'watch_search' and 'check_watches' or 'list_due_follow_ups' and 'schedule_follow_up' as they handle different actions.
All tool names follow a consistent verb_noun pattern using underscores (e.g., search_jobs, save_job, track_application). Even 'unwatch' and 'set_profile' fit the convention. No mixing of styles or vague verbs.
14 tools is well-scoped for a career assistant covering job search, application tracking, watches, profile management, and fit scoring. Each tool serves a clear role without being excessive or insufficient.
The tool set covers core workflows: searching, saving, applying, tracking, scheduling, and scoring. Minor gaps exist, such as no tools to list saved jobs or list all applications, which an agent might need to navigate the pipeline programmatically.