mcp-job-intel
This MCP server enables job intelligence retrieval and ranking, providing tools to list jobs, fetch details, access corpus statistics, and score candidates against job descriptions using staged retrieval to optimize LLM context usage.
README
Agentic Job Intelligence Pipeline (MCP + LLM)
An agent-driven pipeline that uses the Model Context Protocol to orchestrate external
tools for structured data retrieval, with an LLM scoring layer that ranks unstructured
job descriptions against a candidate profile. Context-window pressure is handled with a
staged metadata-first retrieval strategy (pipeline.py), and the same tools are also
exposed to a genuine tool-calling agent with its own planning loop (agent.py) and to a
REST + WebSocket API (api.py). See docs/ for the full write-up.
Run it
pip install -r requirements.txt
python pipeline.py --benchmark # token comparison, zero API calls
python pipeline.py --dry-run # real MCP subprocess handshake, no LLM
export OPENAI_API_KEY=sk-...
python pipeline.py --top 8 # fixed 3-stage pipeline
python agent.py --dry-run # agent tool discovery, no LLM calls
export OPENAI_API_KEY=sk-...
python agent.py --top 8 # tool-calling agent with a planning loop
python eval.py --prefilter-only # stage-1 recall, deterministic half, no key needed
pytest -q # in-process MCP server, no key needed
uvicorn api:app --reload # REST + WebSocket layer, http://localhost:8000
curl localhost:8000/health
curl -X POST localhost:8000/rank -H 'content-type: application/json' -d '{"use_llm": false}'
Measured result
150-job corpus, shortlist of 8. prefilter() applies tag/title, seniority (drop senior
when candidate years < 4), and location (candidate's preferred city, alias-normalised, or
remote) gates, which cut the survivor count from 150 to 31:
| Strategy | Prompt tokens | vs naive |
|---|---|---|
| A — send all 150 full descriptions | 67,360 | — |
| B — metadata-first, then fetch 8 | 13,802 | 4.9× cheaper |
| C — prefilter → metadata → fetch 8 | 6,258 | 10.8× cheaper |
Reproduce with python pipeline.py --benchmark. Real numbers from this repo's
data/jobs.json today, not placeholders. Token counts use tiktoken's o200k_base
encoder (what gpt-4o / gpt-4o-mini actually use) — exact, not estimated. The old
chars ÷ 4 heuristic overestimated the naive-strategy cost by 17.4% on this corpus;
benchmark()'s heuristic_vs_real_tokens field reproduces that comparison.
Architecture
MCP SERVER (stdio subprocess) MCP CLIENT / pipeline.py
--------------------------------- ------------------------------------
tool list_jobs -> metadata <---- Stage 0 prefilter() [0 tokens]
tool get_job_details -> full text Stage 1 shortlist [~5k tokens]
tool get_candidate_profile Stage 2 score [~4k tokens]
tool corpus_stats
resource jobs://schema Meter tracks tokens per stage
prompt rank_jobs
The staged retrieval argument
Naive: hand every full description to the model and ask it to rank. Three problems.
- Cost — 79k prompt tokens per run, and it grows linearly with the corpus.
- Ceiling — past a few hundred postings it exceeds the context window outright. Not slow: impossible.
- Quality — long-context recall degrades in the middle of a large prompt, so the ranking gets worse as you add more candidates.
Staged retrieval, cheapest filter first:
| Stage | Mechanism | Cost | Why here |
|---|---|---|---|
| 0 | Deterministic tag/title/location filter in Python | free | Never let a model read what if could discard. 150 → 91. |
| 1 | LLM sees ~55 tokens of metadata per job, picks top 8 | ~5k | High-recall screen. Instructed to over-include, because stage 2 can reject. |
| 2 | Full descriptions for the 8 survivors only | ~4k | Full fidelity, paid for once, only where it changes the answer. |
The generalisable principle — and the thing to say out loud in an interview — is cascade by cost: order your filters cheapest-first, and set each stage's threshold for recall rather than precision, because a later stage can still reject but nothing can recover what an early stage dropped.
Agentic layer (agent.py)
pipeline.py is a fixed script: prefilter, then always shortlist, then always score.
agent.py hands the model the same MCP tools via OpenAI function calling and lets it plan
its own path — a genuine tool-calling agent, not a hardcoded sequence:
- Structured final answer as a tool call. The agent doesn't "answer in JSON and hope" —
finishing means calling a synthetic
submit_rankingstool whose parameter schema isschemas.RankingResult. Invalid arguments come back as a validation error the model can read and correct, for a bounded number of retries. - Tool failures degrade, they don't crash. Any MCP tool exception becomes a normal
{"error": ...}tool result fed back to the model, so it can route around a bad call instead of taking the whole run down. - A model that never converges still returns something. If it exhausts its step/retry
budget without valid output, the agent falls back to the same deterministic
prefilter → shortlist → scorelogic aspipeline.py, and reportsfallback_used: true. - Transient API errors get their own retry, via
tenacity, separate from the schema-retry loop above — a bad connection and a bad answer are different failure modes.
See docs/CODE_WALKTHROUGH.md for the step-by-step loop.
REST + WebSocket layer (api.py)
A FastAPI service wraps the MCP tools, pipeline.py, and agent.py so they're reachable
over HTTP instead of only as CLI scripts:
| Endpoint | What it does |
|---|---|
GET /health |
liveness check |
GET /jobs, GET /jobs/{id} |
metadata list / full detail, same no-description invariant as the MCP tool |
GET /stats |
corpus stats |
POST /rank |
run the ranking pipeline (agentic planning loop by default, or the fixed staged pipeline); use_llm: false runs the free deterministic half only |
WS /ws/rank |
same as POST /rank, but streams one event per agent step as it happens, instead of a single response at the end |
One MCP stdio session is opened once at startup and shared behind a lock (api.MCPSession)
rather than spawning a subprocess per request — a deliberate simplification over a real
connection pool, documented as such in api.py's module docstring, not oversold as an
actual distributed system. Each request gets a correlation id (request_id), threaded
through logs and every streamed event, so a run can be traced across the async hops.
MCP notes worth knowing cold
- Why it exists: N models × M integrations becomes N + M. One protocol, JSON-RPC 2.0 over stdio or Streamable HTTP.
- Tools vs resources vs prompts: model-controlled / application-controlled / user-controlled. Getting this trio right is a common interview differentiator.
CallToolResultshape:content(blocks),structured_content(typed, wrapped as{"result": ...}for non-object returns),is_error. Seepipeline.call().- Tool design is API design for a non-human caller.
list_jobsandget_job_detailsare split because that split is what enables staged retrieval. Docstrings are the tool description the model reads — vague docstring, wrong tool choice. - Batch parameters over scalar ones:
get_job_details(job_ids: list[str])costs one round trip;get_job_detail(job_id: str)costs eight.
Known limits
- Shortlist recall (does stage 1 keep the labelled-relevant jobs that survive prefilter?) needs a real
OPENAI_API_KEYto measure —python eval.py --top 8runs it; not run here for cost reasons. - The corpus is synthetic. Real postings are messier — HTML, duplicates, stale listings.
- No caching across runs, so repeated invocations pay stage 1 again.
Stage 1 recall — measured, not assumed
data/relevance_labels.json has 20 job IDs a human would call relevant to the candidate,
picked with a documented, reproducible rubric (see the file). eval.py checks two things
separately:
prefilter_recall— of the 20 labelled-relevant jobs, how many survive the deterministic prefilter? Free, no API key:python eval.py --prefilter-only→ 20/20, recall 1.0. The label rubric is a strict subset of prefilter's own gates, so this confirms prefilter isn't silently dropping the target role, rather than assuming it.shortlist_recall— of those, how many also survive the LLM shortlist at thetopyou actually run with?python eval.py --top 8— needsOPENAI_API_KEY, a real model call, so it isn't run in this repo; run it yourself when you have a key.
Your TODOs
Done: 150 → 31 survivors, 10.8× reduction vs. naive.prefilter()— add seniority and location gates. Re-run--benchmark, record the number.SwapDone: heuristic overestimated by 17.4%.approx_tokensfor realtiktokencounting; note how far the ÷4 heuristic was off.Build a 20-job labelled relevance set and measure stage 1 recall.Done for the free half (prefilter_recall= 1.0); the paid half (shortlist_recall) is wired up ineval.py --top 8, run it with your own key.Wire the server into Claude Desktop's MCP config and call it by hand.Config snippet and restart instructions are indocs/OVERVIEW.md— actually registering it happens in your own Claude Desktop app, not something this repo can do for you.
Docs
docs/OVERVIEW.md— what this project is, what problem it solves and why, architecture, Claude Desktop wiring, local testability, known limitations.docs/CODE_WALKTHROUGH.md— every module, function by function.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。