# Jobops

> A self-hosted Model Context Protocol server for the full job-search loop — portal scanning, JD evaluation, tailored resume + cover PDFs, outreach drafting, story bank, negotiation brief — all driven from your MCP-aware chat client (Claude Desktop, Cursor, any client that speaks streamable-HTTP MCP).

- **Type:** MCP server
- **Install:** `agentstack add mcp-mohith-das-jobops`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [HireBridge](https://agentstack.voostack.com/s/hirebridge)
- **Installs:** 0
- **Category:** [Search](https://agentstack.voostack.com/c/search)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [HireBridge](https://github.com/HireBridge)
- **Source:** https://github.com/HireBridge/jobops
- **Website:** https://www.npmjs.com/package/@mohith_das/jobops

## Install

```sh
agentstack add mcp-mohith-das-jobops
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# jobops

A self-hosted **Model Context Protocol** server for the full job-search loop — portal
scanning, JD evaluation, tailored resume + cover PDFs, outreach drafting, story bank,
negotiation brief — all driven from your MCP-aware chat client (Claude Desktop, Cursor,
any client that speaks streamable-HTTP MCP).

The chat is the brain. This server executes the mechanical work and hands every artifact
back as an `http://localhost:7891/...` link.

> **Status:** early. Works. APIs may still move pre-1.0.

---

## Quickstart

```bash
# 1. Scaffold your working directory (cv.md, profile.yml, portals.yml, modes/*.md + SQLite DB)
npx @mohith_das/jobops init

# 2. Open cv.md, config/profile.yml, portals.yml and replace every  placeholder.
#    (Optional: tune modes/*.md — rubric, tailoring rules, outreach tone — your edits win.)

# 3. Rebuild the career_packet from your now-real cv.md
#    (or just re-run `init` — it auto-reseeds when it detects cv.md changed)
npx @mohith_das/jobops reseed

# 4. Confirm everything is wired
npx @mohith_das/jobops doctor

# 5. Boot the server (Chromium auto-installs on first run)
npx @mohith_das/jobops start
#  ▷ jobops listening on http://127.0.0.1:7891

# 6. Get a copy-paste config block for your MCP client
npx @mohith_das/jobops connect

# 7. Paste a job URL or pasted JD into your chat — the chat calls evaluate_job, draws on
#    your rubric + career_packet + tailoring rules, and walks the rest of the workflow.
```

That's the loop. Everything else (warm-intro finder, story bank, negotiation brief,
batch rater, scheduler, …) is wired in but optional.

### Editing the career packet — two safe directions

Your `career_packet` is the source of truth the chat uses to score JDs and draft materials.
You can drive edits from **either** direction, and **neither one silently destroys the
other** — both are explicit:

**1. Chat-driven (the packet is your edit surface).** Ask the chat to change a tagline,
remove a project, tighten a bullet — it calls `update_career_packet`, which writes a new
version and **marks the packet user-edited**. From then on a plain `reseed` will **refuse**
to overwrite it (it warns and tells you to pass `force` or sync first). Section edits are
ergonomic — "change my tagline" only re-sends Section 2, not the whole packet:

```text
update_career_packet section="2" section_content='- **Builder PM** — "ships product with engineering teeth"'
```

For surgical, **item-level** changes there's `edit_packet_item` / `remove_packet_item` —
address a section (`projects`, `skills`, `taglines`, or a number) and one item (by index or a
matching substring) to change or drop a single bullet/project/skill/tagline without touching
the rest. Every change still versions the packet, and `edit_packet_item` runs the
visa-leakage scan on the new text. `remove_packet_item` echoes exactly what it removed. Since
every change is a version, **nothing is truly lost** — `restore_packet_version` lists history
and brings any prior version back (restore is itself reversible).

**2. File-driven (cv.md / profile.yml are the source).** Edit `cv.md` /
`config/profile.yml`, then **reseed** to rebuild the packet from them:

```bash
npx @mohith_das/jobops reseed            # safe: refuses if the packet has chat edits not in cv.md
npx @mohith_das/jobops reseed --force    # rebuild from cv.md anyway (drops chat edits)
```

**Bringing the two back in sync.** When you've been editing in chat and want `cv.md` to
catch up, run `sync_packet_to_cv` — it writes the packet back into `cv.md` + `profile.yml`
so the source files reflect your chat edits (and a later `reseed` reproduces them instead of
clobbering them). So the two directions are symmetric: **reseed** (cv.md → packet) and
**sync-back** (packet → cv.md), both explicit, neither automatic.

`reseed` writes a NEW active version (previous demoted, history kept). `doctor` reports when
the packet is chat-edited (expected — not a nag) vs when `cv.md` changed under a file-driven
packet. Standing policy with no CV/profile field (naming conventions, rendering rules, custom
guardrails) lives in `modes/career_packet.md` Section 9, which reseed always preserves.

> **Operator's guide / project memory:** [`docs/PROJECT_MEMORY.md`](docs/PROJECT_MEMORY.md)
> is a single self-contained reference (architecture, every tool, env vars, setup, the
> template system, sampling/elicitation/auth, hard rules, troubleshooting). Drop it into your
> MCP client's project memory so you can ask "how do I X?" and get answers.

---

## What it does

Two systems merged into one MCP server:

- **Evaluation + materials side** — port of [santifer/career-ops](https://github.com/santifer/career-ops):
  6-block A–F (+G legitimacy) report, archetype detection, ATS-friendly HTML→PDF resume
  + cover generation, story bank, negotiation playbook.
- **Pipeline side** — port of a personal Postgres + n8n pipeline ("JSA"): Greenhouse /
  Ashby / Lever / Workday pollers + closed-board Playwright scrapers, content-hash
  dedupe, batch LLM rater with strict-JSON parsing, warm-intro / founder DM drafter,
  visa signal from DOL OFLC H1B data.

Everything lives in a single Node process with a single SQLite file. No external
Postgres, no n8n, no cloud anything. Bring your own LLM key (Gemini free tier by
default, DeepSeek optional) if you want the API/batch paths; chat-mode tools work
without one.

> **Not affiliated with or endorsed by santifer's career-ops.** This is an independent
> project that ports + adapts the publicly-released MIT-licensed templates and rubric
> shape into the MCP transport surface. See the [Attribution](#attribution) section.

---

## Tools (58 — one MCP `tools/list` call away)

| Group | Tools |
|---|---|
| **Evaluation** | `evaluate_job`, `batch_evaluate`, `get_top_jobs`, `evaluate_training`, `evaluate_project` |
| **Materials** | `generate_materials`, `render_pdf` (PDF / `.tex` / `.docx`), `get_report` |
| **Tracker** | `get_tracker`, `update_status`, `mark_ready_to_apply`, `delete_jobs` (soft-delete → trash), `restore_jobs`, `list_trashed`, `purge_jobs` (permanent, backup-first) |
| **Sourcing** | `scan_portals` (Greenhouse + Ashby + Lever + Workday + Amazon + Google + generic Playwright) |
| **Outreach** | `find_warm_intros`, `find_founders`, `draft_outreach`, `draft_followup`, `draft_reply`, `get_outreach_queue`, `update_outreach`, `get_followups_due` |
| **Contacts** | `add_contacts` (upsert from chat), `export_contacts` (CSV+JSON backup), `import_contacts` (merge, non-destructive), `delete_contacts` (soft-delete, recoverable) |
| **Interview / offer** | `extract_stories`, `get_story_bank`, `negotiation_brief` |
| **Research** | `deep_research`, `enrich_company`, `daily_digest` |
| **Profile + ops** | `get_career_packet`, `update_career_packet` (whole-doc), `edit_packet_item` / `remove_packet_item` (one bullet/project/skill/tagline), `restore_packet_version` (history + restore), `reseed_career_packet` (safe by default), `sync_packet_to_cv`, `update_profile` (elicitation), `cost_estimate`, `doctor` (read-only health) |
| **Federation** | `compile_career_packet`, `get_career_packet_json` (public-safe), `sync_to_livingcv`, `generate_embeddings`, `get_embeddings`, `broadcast_signal`, `get_federation_status`, `update_jobops` |
| **Apply (preview only — never submits)** | `apply_prefill` |
| **Visa (optional, can be hidden)** | `visa_signal`, `import_h1b`, `import_linkedin` |
| **Scheduler (opt-in cron, off by default)** | `scheduler_status`, `scheduler_enable`, `scheduler_disable` |

Six MCP **resources** carry the editable behaviour — rubric, report_format,
tailoring_rules, outreach_tone, negotiation_playbook, career_packet — all loaded from
`modes/*.md` and live-reloaded on edit. Tune scoring or tone without touching code.

> **Tip:** ask the chat to run **`doctor`** anytime — it's a read-only health report (same
> checks as the `npx @mohith_das/jobops doctor` CLI command) covering packet ↔ cv.md sync state,
> LLM provider/key, sampling + auth posture, active template, modes, visa scoring, and the
> public base URL. Handy for "is my server wired right?" without leaving chat.

---

## Trash & recovery (jobs)

Jobs follow the same non-destructive, recoverable, echo-before-delete philosophy as contacts
and the career packet. **Soft-delete is the default; hard delete is always explicit, confirmed,
and backup-first.**

- **`delete_jobs`** moves 1..N jobs to the **trash** (by `job_ids` and/or by `statuses` — e.g.
  trash everything in `skip`/`discard`/`sourced`). Trashed jobs drop out of the tracker,
  `get_top_jobs`, and batch rating but are **retained and restorable**. It echoes exactly which
  jobs (title + company) were trashed. This is recoverable — *not* a hard delete.
- **`list_trashed`** shows what's currently in the trash (title, company, score, prior status,
  when trashed) so you can review before restoring or purging.
- **`restore_jobs`** brings trashed jobs back to their prior state.
- **`purge_jobs`** is the only hard delete: it permanently removes **trashed** jobs (never live
  ones). Pass `job_ids`, or `purge_all: true` (which requires `confirm: true`). A timestamped
  backup of the affected rows is written to the project root **before** deletion; it echoes
  exactly what was permanently removed.

**In the tracker UI** (`/`): the **Status** cell is an inline dropdown (edit in place), and each
row has a compact **Trash** button (soft-delete; the row leaves and the summary cards update
live). A dedicated **[/trash](/trash)** page lists trashed jobs where you can **Restore** an
item, **Delete permanently** one (with a confirm dialog), or **Empty trash** (a clearly-warned,
irreversible hard delete — backup written regardless). UI actions hit the same `/api/*`
endpoints that share the exact core logic the chat tools use, so a job trashed in chat shows up
on `/trash` and vice-versa.

---

## Filtering, search & pagination (tracker)

At 1000+ jobs the tracker doesn't ship every row to the browser — it **paginates server-side**
(SQL `WHERE`/`LIMIT`/`OFFSET` + a `COUNT`, never in-memory). The dashboard at `/` has:

- **Filters (combinable):** multi-select **status**, **min/max score**, **company** (contains,
  with a datalist of present companies), **role** and **seniority**, and a **show trashed** toggle.
- **Search:** debounced, case-insensitive substring on **title or company**.
- **Pagination:** page-size selector (25/50/100, default 50), first/prev/next/last + jump-to-page,
  and a "N matching · showing X–Y" indicator.
- **Sort:** click the **Score / Company / Discovered** headers (default score ↓).
- **Shareable URLs:** every filter/sort/page is in the query string
  (`/?status=applied&min_score=70&q=engineer&sort=score&page=2`) — bookmarkable and survives refresh.
- **The summary cards stay TRUE TOTALS** — the whole active pipeline, independent of the current
  filter/page. Inline status edits and trashing keep working within a filtered/paged view (the
  row updates/leaves and counts refresh without losing your place).

The **same query powers the `get_tracker` MCP tool**, so chat can ask for slices too — e.g.
*"show me applied jobs scored over 70 with 'engineer' in the title."* `get_tracker` accepts
`statuses[]`, `min_score`/`max_score`, `company`, `role_category`, `seniority`, `q`, `sort`,
`dir`, `limit`, `offset`, and `show_trashed`, and returns `items` + `total_matching` +
full-pipeline `counts_by_status`.

---

## Scoring without an LLM key — IF your client supports MCP sampling

The scoring tools (`batch_evaluate`, `evaluate_job` `mode="api"`) can run on your
*connected client's model* via **MCP sampling** — same rubric, same strict-JSON contract,
**no separate Gemini/DeepSeek key** — **but only if the connected client advertises the
`sampling` capability** in its initialize handshake.

> ⚠️ **Most clients don't (yet) — including Claude Desktop, as of now.** Claude Desktop
> advertises only its UI extension, never `sampling`. The transport (stdio vs HTTP) is **not**
> sufficient on its own — it's a per-client *capability*. So on Claude Desktop and similar
> clients, batch/api scoring **falls back to the BYO key** (`JOBOPS_LLM_PROVIDER` + key),
> which is expected and correct. Check current support at
> [modelcontextprotocol.io/clients](https://modelcontextprotocol.io/clients).

- **It engages automatically if (and only if) a sampling-capable client connects** — no
  configuration needed. The gate checks both the client's advertised `sampling` capability
  **and** that the transport can carry the server→client request (stdio; the stateless HTTP
  transport can't, so it never even tries). When sampling isn't available, scoring uses the
  BYO key; if that isn't set either, `evaluate_job mode="chat"` (the default) still works —
  your chat scores it directly. Fallback is clean (no hang).
- **Bottom line for Claude Desktop users:** set `JOBOPS_LLM_PROVIDER` + the matching key
  for `batch_evaluate` / `evaluate_job mode="api"`. (Plain `mode="chat"` needs no key.)
- **Cost.** When sampling *is* used it runs on the client's model, so the cost is **borne by
  the client**; `cost_estimate` records those calls flagged client-borne ($0 server cost).
- Run the `doctor` tool to see the **live** state — it reports whether your current client
  advertised sampling or whether you're on the BYO-key path.
- Set `JOBOPS_SAMPLING=false` to force the BYO-key path even when sampling is available.

## Frictionless profile setup (MCP elicitation)

`update_profile` uses **MCP elicitation** (form mode) so your client can collect identity
fields + per-archetype taglines through a structured form — no hand-editing YAML. On
accept it writes `config/profile.yml` and reseeds the career packet in one step.

Sensitive inputs (your LinkedIn export path, credentials) use **URL-mode elicitation**
(2025-11-25): the server hands you a one-time local URL where you enter the value
directly, so it **never passes through the MCP client / chat transcript**. `import_linkedin`
uses this when you omit `path` and your client supports it.

Both are capability- and transport-gated (like sampling, elicitation is a server→client
request that needs a stdio connection). Clients without elicitation support — and all HTTP
clients — fall back to the file-based + argument paths (`update_profile fields=…`, edit
`config/profile.yml`, pass `import_linkedin path=…`), which work exactly as before.

---

## Designed to be made yours

The defaults assume nothing about your location, citizenship, role, or industry. Every
behaviour-shaping piece is a markdown file you can rewrite. **`init` copies these into
`/modes/` so they're yours to edit** — the loader reads your project-root
copy first and falls back to the bundled default, so you never touch the package install.
A re-`init` never clobbers an edited copy (it warns and keeps your edits); `doctor` reports
which files are user-overridden vs bundled.

| You can change… | By editing… |
|---|---|
| Scoring dimensions + weights | `modes/rubric.md` |
| 6-block report shape | `modes/report_format.md` |
| Resume/cover tailoring rules | `modes/tailoring_rules.md` |
| Outreach tone + char caps | `modes/outreach_tone.md` |
| Negotiation scripts + framework | `modes/negotiation_playbook.md` |
| Your bullet/project bank | `modes/career_packet.md` (or via `update_career_packet`) |
| Per-archetype taglines | `config/profile.yml` → `taglines:` (auto-fills career-packet Section 2 on reseed) |
| Tracked companies + filters | `portals.yml` |
| Identity + target roles | `config/profile.yml` |

### Non-US users / non-sponsorship cases

Visa scoring is **fully optional**. Set:

```bash
export JOBOPS_VISA_SCORING=false
```

When off:

- `score_total = round(0.6 · resume_fit + 0.4 · taste_fit)` (server-side authoritative)
- The `visa_signal`, `import_h1b`, `import_linkedin` tools are hidden from `tools/list`
- `score_visa_fit` is stripped from `get_top_jobs` items and the eval-report HTM

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [HireBridge](https://github.com/HireBridge)
- **Source:** [HireBridge/jobops](https://github.com/HireBridge/jobops)
- **License:** MIT
- **Homepage:** https://www.npmjs.com/package/@mohith_das/jobops

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-mohith-das-jobops
- Seller: https://agentstack.voostack.com/s/hirebridge
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
