Install
$ agentstack add mcp-mohith-das-jobops ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
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
# 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:
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:
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:
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, reportformat, tailoringrules, outreachtone, negotiationplaybook, 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_jobsmoves 1..N jobs to the trash (byjob_idsand/or bystatuses— 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_trashedshows what's currently in the trash (title, company, score, prior status,
when trashed) so you can review before restoring or purging.
restore_jobsbrings trashed jobs back to their prior state.purge_jobsis 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.
- 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
doctortool 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=falseto 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:
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_linkedintools are hidden fromtools/list score_visa_fitis stripped fromget_top_jobsitems 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
- Source: HireBridge/jobops
- License: MIT
- Homepage: https://www.npmjs.com/package/@mohith_das/jobops
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.