Skip to main content
Glama
rani700

CareerPilot

by rani700
README.md
# CareerPilot ๐Ÿงญ

![tests](https://github.com/rani700/careerpilot/actions/workflows/tests.yml/badge.svg)

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 dashboard โ€” the hiring file](docs/dashboard.png)

`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

A3.7/5.0

Scored across 14 tools

Disambiguation5/5

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.

Naming Consistency5/5

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.

Tool Count5/5

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.

Completeness4/5

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.

Maintenance

ActivityStale
ResponsivenessNo issues