Skip to main content
Glama
gzchenhao

OpenHire — Real Job Postings, Ghost Jobs Scored

OpenHire · 开聘

A job-search radar for your AI assistant — first-party listings, every posting's real age, and your résumé never touches our servers. 让 AI 助手替你盯岗的求职雷达 —— 一手职位、岗位在架时长打分,简历不经过我们的服务器。

MCP 1.0 privacy: local-first python ≥ 3.11 license: MIT 139 employers hiring OpenHire on Glama

Is your company in here, and you did not put it here? Your postings are in this index because your own careers page serves them publicly. We can measure how long a role has been open; we cannot see why, and a long-open role is a question, not a verdict. Claim your company — free, no payment, ever — and say why in your own words, or ask us to remove you and we will, without arguing. 贵司被收录了、而且不是贵司提交的?点这里认领,免费, 可以用自己的话解释,也可以直接要求我们移除。

What your agent actually sees

You ask your assistant a question in plain language. It calls search_jobs, and every row comes back carrying the employer's real posting date — so the agent can reason about staleness instead of guessing.

You: Any senior Python roles that are actually still open? Skip the stale ones.

// one row from search_jobs — trimmed to the fields that matter here
{
  "title":        "Senior Python Engineer",
  "company":      "MongoDB",
  "datePosted":   "2026-03-31",   // from the employer's ATS, not a board's refreshed label
  "days_open":    166,
  "ghost_score":  0.61,           // pure f(relist_count, datePosted) — frozen by a test
  "apply_channel":"https://boards.greenhouse.io/…",   // straight to the employer
  "verified_at":  "2026-09-02T09:47:10Z"
}

Assistant: This one has been open 166 days with a ghost_score of 0.61 — I'd deprioritise it. Here are four posted in the last three weeks instead…

ghost_score measures how long a posting has been open, not whether the employer still intends to hire. A long-open role can equally mean "hard to fill". Treat it as a reason to ask, not a verdict.


An MCP server that turns any MCP-speaking assistant — Claude, Cursor, Windsurf, Cline, ChatGPT via connectors — into a private radar for AI / Infra, autonomous-driving and embodied-AI jobs — pulled straight from 139 employers' own career sites and public ATS APIs (Greenhouse / Lever / Ashby / 北森 Beisen / Moka), across the US, Europe and China (Waymo, Figure, Zoox — and Unitree, XPeng, UBTECH, Mech-Mind…). No account. No signup. No résumé upload. Ever.

Three things a job board won't do for you:

  • Surfaces how long each role has really been open. Every listing carries a ghost_score aged off the employer's real posting date — the "2 days ago" a board shows you can be 300 days old in the ATS.

  • Structural privacy, not a pinky-promise. There is no résumé field in the protocol; a CI test fails the build if anyone adds one. Matching runs on your machine — only an anonymous fingerprint reaches the server.

  • Ranking you can't buy. Order is a locked pure function of (match, freshness). No sponsored slots, no bidding — the signature is frozen by a test.

This is the 「哨兵 / Sentinel」 reference implementation — see design_handoff_openhire_v01/README.md for the full protocol spec.


Quickstart — under a minute

Nothing to install, and no crawl to sit through. Add this to your MCP client's config:

{ "mcpServers": { "openhire": { "command": "uvx", "args": ["openhire@latest", "serve"] } } }

Then ask your assistant for a job. That is the whole setup. The server downloads the ~25 MB public index by itself in the background on first start, so searches fill in within a couple of minutes while you are already talking to it. No account, no signup, no résumé upload.

The one prerequisite is uv (it provides uvx). Without it the config above fails with nothing but "server failed to start", so install it first:

curl -LsSf https://astral.sh/uv/install.sh | sh     # macOS / Linux
irm https://astral.sh/uv/install.ps1 | iex          # Windows PowerShell

On Claude Desktop you can skip even that — download openhire-0.6.0.mcpb and double-click it. No terminal, no Python, no uv.

Per-client config paths and the trade-offs of uvx vs a one-time install are in Works with below.

pipx install openhire      # keeps it isolated and puts `ohp` on your PATH
ohp bootstrap              # 139 employers · ~16k live postings · no account

ohp search --required-skills rust,k8s --remote --role-family engineering
ohp search --currency CNY --role-family engineering   # e.g. CN 智驾 / robotics roles

ohp bootstrap downloads the public snapshot (~25 MB, no account) and then runs one incremental crawl to refresh verified_at and catch delistings. The crawl is the slow part — a line per employer, 20+ minutes on a cold index — and you can stop it once the snapshot is in; the index is already usable, just verified as of the last weekly refresh rather than today. The MCP path above needs none of this: serve fetches the snapshot by itself in the background.


Related MCP server: Job Application MCP

Works with

All clients use the same MCP entry. The config below works in every MCP client and pulls the package on demand — but it does need uv present first.

New to MCP? Two shortcuts before the config below. Claude Desktop — download openhire-0.6.0.mcpb and double-click it. No terminal, no Python. Cursor / Claude Code — paste this to your agent: "Install the MCP server at github.com/gzchenhao/openhire. Install uv first if it is missing, then add uvx openhire@latest serve to my MCP config and tell me which file you changed."

Prerequisite: uvx ships with uv. Without it the config below fails with nothing but "server failed to start" in your client — install uv first:

curl -LsSf https://astral.sh/uv/install.sh | sh     # macOS / Linux
irm https://astral.sh/uv/install.ps1 | iex          # Windows PowerShell
{ "mcpServers": { "openhire": { "command": "uvx", "args": ["openhire@latest", "serve"] } } }

First start downloads a ~25 MB index in the background; searches fill in within a few minutes.

Stuck? Run ohp doctor. It checks the three things that all look identical from the chat window — uv missing, no index yet, server configured but not enabled — and reads every client config it can find. It runs in your terminal, which matters: if the client never started our server, nothing we wrote inside it can reach you.

Editing the config may not be the last step. Several clients require you to enable or trust a newly added server before its tools load — the config is saved, the server never starts, and the only symptom is that your assistant does not seem to know about the tools. If a search does nothing, open your client's MCP/connectors panel and check that openhire is listed and switched on. (Claude Desktop needs a full quit and reopen; Cursor and Windsurf pick it up on reload; some clients show a per-server toggle.) 改完配置不一定就完事:部分客户端需要你在设置里手动「信任 / 启用」这个 server, 工具才会加载。症状是配置明明在、助手却完全不知道有这些工具。

What uvx costs you, every time. uvx resolves the package on each invocation — measured at 7–8 s per call even with a warm cache. That is paid on every MCP session start and every CLI command. It buys you never having to manage an install. If you would rather pay once:

pipx install openhire     # then use "command": "ohp", "args": ["serve"] — process-start latency

@latest also means your tool surface can change under you without warning. Pin it when that matters: "args": ["openhire==0.6.0", "serve"].

The server auto-downloads the public job snapshot on first run if the index is empty, so ohp bootstrap is optional. If you ran pipx install openhire, "command": "ohp" works too.

Claude Desktop%APPDATA%\Claude\claude_desktop_config.json (macOS: ~/Library/Application Support/Claude/); quit & reopen after editing:

{ "mcpServers": { "openhire": { "command": "ohp", "args": ["serve"] } } }

Cursor~/.cursor/mcp.json (or a project .cursor/mcp.json):

{ "mcpServers": { "openhire": { "command": "uvx", "args": ["openhire", "serve"] } } }

Windsurf~/.codeium/windsurf/mcp_config.json:

{ "mcpServers": { "openhire": { "command": "uvx", "args": ["openhire", "serve"] } } }

First start downloads the ~25 MB public snapshot (jobs/companies only) — give it a moment. To refresh later run ohp bootstrap --force or ohp ingest. On Windows Claude Desktop from the Microsoft Store, the config is under …\Packages\<Claude package>\LocalCache\Roaming\Claude\.

Hosted / remote: ohp serve --transport streamable-http --host 0.0.0.0 --port 8000 exposes http://host:8000/mcp (also --transport sse). A Dockerfile is included.


What it does

Tool

What it gives you

search_jobs

Hard-filter the live index; every result carries verified_at, datePosted, days_open, ghost_score, remote_scope, eligible_regions, apply_channel. Filter by required_skills (AND), role_family, remote_scope, min_salary + currency.

watch_intent

Register a standing intent once — new matching jobs are waiting next time you check, even after you close the terminal. Accepts required_skills / role_family so sales / solutions roles stay out.

check_watches

Pull the matches that are new since your last check (client-pull; stdio has no push).

authorize_application

One explicit confirmation per job. It records your authorization and returns the employer's own application URL — you apply as yourself. It cannot accept a résumé.

get_company_info

Aggregate, anonymous trust signals for one employer (ghost_score_avg, active_jobs, index_built_at). Never any candidate data.

Optional, entirely local: ohp init --scan <dir> derives a skill fingerprint from your own repos. You never write a résumé; the code never leaves your machine — only an anonymous vector does.

For employers: claim your tenant

If your company is in this index, the listings came from your own public ATS — we did not ask, because we did not need to. What we cannot know is your side of it: whether a role is an evergreen talent pool rather than a stale req, or how fast you actually reply.

Claim it中文表单). Free, verified by corporate identity — a GitHub org membership or a reply from a corporate domain — and never by payment. We answer within 3 business days, claiming leads to no paid follow-up of any kind, and your proof is used to verify and then nothing else.

No GitHub account? Email gdchenhao@qq.com with "Employer claim" and your company name in the subject — sending from your corporate domain is itself the verification — or have anyone file the form on your behalf, since what we verify is the company and not the filer. 没有 GitHub 账号?直接发邮件到 gdchenhao@qq.com,用贵司企业邮箱发出来即完成身份核验。

A claim gets you:

  • Your own note, attributed to you, beside the roles you name. We can see how long a role has been open; we cannot see why. An evergreen talent pool, a genuinely hard-to-fill role, and a neglected one look identical from outside, and only you can tell them apart.

  • evergreen / hard to fill / closed status on specific titles. A closed role stays visible while your ATS still serves it — we do not hide what your own site returns — but it is marked closed on your word so nobody else applies.

  • A correction if your ATS's date field means "requisition opened", not "went live". That one misreading makes every role you have look years old, and it is not your fault.

  • response_sla_days on every one of your postings, including ones you post later

  • claimed: true on your company, with the date

It does not get you rank, and it does not lower your ghost_score. Ordering is a locked pure function of (match, freshness) and the score is a pure function of (relist count, posting age); tests freeze both and assert the claim path touches neither. Your note sits beside the score and explains it. We would rather show a high score with your explanation than a quiet score somebody paid for.

Asking to be removed entirely is also fine, and we will not argue about it.

Verified claims live in src/openhire/seed/claims.py — in the repo, not in a private database — so each one is a reviewable diff, and the weekly rebuild re-applies them instead of quietly dropping them.

Keeping it current

The index refreshes weekly, so a search can be up to seven days behind. When the user is about to act on one employer and wants today's truth:

ohp refresh unitree          # ~1 minute · at most one crawl per employer per 6 hours

Over MCP this is the refresh_index tool. Three rules are built in, not advisory:

  • One employer per call. A full crawl is 20+ minutes and no client waits that long. An ambiguous word (robot → 11 matches) is refused with the candidate list, never fanned out.

  • Six-hour throttle per employer, checked before any network call, so a too-soon request costs the ATS nothing and returns last_refreshed_at instead of an error.

  • Not for speculative or looped calls. Each one hits somebody else's public endpoint.

That last point is the whole design constraint: our own crawl is a weekly batch we control, and handing refresh to callers turns it into our users hitting their endpoint on our behalf. The throttle is what keeps that boundary ours to keep rather than ours to spend.

Reading the three dates

Every row carries three timestamps that answer three different questions. Read together they separate an abandoned requisition from one somebody is still tending:

field

question it answers

verified_at

did the employer's ATS still return this the last time we looked?

datePosted / days_open

how long has it been open? ghost_score ages off this

updated_at / days_since_update

when did the employer last touch it? Null when their ATS does not report one (Ashby, Lever and Beisen do not; Greenhouse and Moka do) — read null as "unknown", never as "abandoned"

ghost_score = 1.0 alone is not a verdict. Open 367 days and untouched for 367 days reads as abandoned; open 327 days but touched 13 days ago reads as a tended evergreen req. Among the rows where the ATS actually reports a last-touched date, 67% of ghost_score >= 0.99 postings were touched by the employer within the last 30 days (measured 2026-09-19; the live figure is pct_ghost_hi_touched_within_30d in docs/numbers.json, which is regenerated every refresh). ghost_reason spells out which input drove the score ("age only: open 367d, never relisted").

None of these measure intent. A long-open role can equally mean hard-to-fill — treat the numbers as a reason to ask, not a verdict.

Asking about one employer

search_jobs(company=...) takes whatever the user actually said — an id (unitree), or any part of the name in either language (宇树, Unitree, XPeng). A name this index does not carry comes back as the empty-result object naming the company, never as an unfiltered search.

ohp search --company 宇树 --role-family engineering
ohp search --company waymo --distinct          # one row per role, not one per city

--distinct (collapse_role_group over MCP) keeps one row per role_group and adds role_group_size. About 20% of a page is the same role listed once per city; folding is opt-in because each city row has its own job_id and apply_channel, which matters under a location or visa constraint.

The five protocol fields

Every listing is valid schema.org/JobPosting, plus:

  • verified_at — last moment confirmed live on the employer's own site

  • sourceemployer_site | ats_public_api (never a job board)

  • ghost_score — 0–1 listing-activity signal, aged off the real posting date (lower = fresher). A noise filter, not an accusation: long-open listings are often evergreen talent pools or slow pipelines — the score simply lets agents down-rank low-activity noise

  • response_sla_days — the employer's OWN committed reply window. Null on almost every row, and that null is meaningful: it is set only when an employer claims their tenant. We never infer or estimate it, because we cannot observe a reply even in principle — the application deep-links to the employer and never touches this server. Read null as "no employer has claimed this tenant", not as "missing" or "slow". Employers: claim yours — free, verified by corporate identity, never by payment. It buys a verified badge, the ability to correct listing status (an evergreen pool carrying an unfair staleness score, say), and this field. It does not buy rank: ranking is a locked pure function of (match, freshness), and a test freezes that signature.

  • apply_channel — always the employer's own application URL, deep-linked to the specific job

Privacy Policy

Short version: there is no résumé field in the protocol, matching runs on your machine, and the only user-originated value the server ever stores is an anonymous client-generated fingerprint. No analytics, no telemetry, no third-party sharing. Full policy: docs/PRIVACY.md.

Privacy model

Résumé / PII upload

never — matching runs locally; a résumé never transits the server, and we never store one

What the server sees

one anonymous, client-generated fingerprint + hard filters

Repo scan

local-only · personal projects · explicit consent · opt-out anytime

Job sources

first-party only: employer career pages + public ATS APIs (Greenhouse / Lever / Ashby)

First-run data — the snapshot vs. fresh

ohp bootstrap (default) downloads a small public index snapshot (a GitHub Release asset — companies + jobs only, zero user data) and then runs one incremental crawl to refresh verified_at / delisting. --fresh skips the snapshot and crawls the public ATS from scratch with the free offline heuristic extractor. Either way: no account, no PII.

Two things that surprise people:

  • The incremental crawl is slow and quiet. On a cold index it can run for 20+ minutes with no output. It is working, not hung. If you only want the data, ohp serve skips it entirely — the server downloads the snapshot on first start and is answering in seconds.

  • The snapshot URL is pinned to the v0.1.0 tag on purpose. It looks stale; it is not. That asset is overwritten in place every Monday by a scheduled workflow, so the URL is a stable address for always-current data. Pinning it to the newest tag would break every client the moment a release is cut.

Three rules this project will never break

  1. Your résumé stays on your machine — it never transits the server, and we never store it.

  2. Ranking is not for sale — it is only f(match_quality, freshness), a locked pure function.

  3. Employers pay only for authorized, delivered outcomes — never for exposure. (v0.1 has no billing at all.)

These are enforced by CI (tests/test_privacy.py, tests/test_ranking.py, tests/test_snapshot.py).

Development

python -m venv .venv && . .venv/Scripts/activate   # Windows
pip install -e ".[dev]"
pytest        # privacy red lines + ranking + snapshot must be green

Set OPENHIRE_DATABASE_URL=postgresql+psycopg://… to run against Postgres instead of the default local SQLite file (~/.openhire/openhire.db).

Roadmap

  • v0.2 – v0.3 (shipped) — CN ATS adapters (北森 Beisen + Moka) · weekly auto-refreshed public snapshot · ghost_score public beta · 139 employers across US / EU / China

  • next — Employer claim + verified badges — employers can reserve their claim today via a corporate-identity GitHub issue (zero-cost now; badges + listing-status control ship next) · response-SLA enforcement (7-day auto-delist) · redacted proof-of-fit — an anonymous, candidate-authorized match summary that travels with an application (skills overlap only; identity never included, résumés still never transit the server)

  • v1.0 — Open, vendor-neutral schema extension for AI-readable job postings

FAQ

Where does the job data come from? Directly from 139 employers' own public ATS APIs (Greenhouse, Lever, Ashby, 北森 Beisen, Moka) — the same endpoints that power their careers pages. No scraping, no third-party job boards. source is always ats_public_api, and verified_at records the last time we confirmed each posting live. The public index is auto-refreshed weekly, so a fresh ohp bootstrap starts from recent data.

Why should I trust ghost_score? It's a pure, open, unpurchasable function — min(1, 0.15·relist_count + staleness) aged off the real ATS posting date, not our crawl date. The formula lives in pipeline/ghost_score.py, is unit-tested, and takes no money as input (red line #2). Long-open, repeatedly-relisted postings score higher; you can always re-rank client-side. Read it as signal-to-noise, not bad faith: plenty of high-scoring listings are legitimate evergreen talent pools. Employers who want their listing activity represented accurately can claim their tenant (see Roadmap).

Does my résumé actually go through the server — really? No. There is no résumé anywhere in the protocol. authorize_application has no résumé/file parameter (it structurally cannot accept one), matching runs on your machine, and the only thing that ever transits the server is a short anonymous fingerprint like #a3f9. This is enforced by tests/test_privacy.py, and the published snapshot carries zero user data (tests/test_snapshot.py).

Does it support China (中国区)? Yes — this is what sets OpenHire apart. Employers on 北森 Beisen (<tenant>.zhiye.com) and Moka (app.mokahr.com) are indexed: 20+ autonomous-driving / robotics / embodied-AI companies including 宇树 Unitree, 小鹏 XPeng, 优必选 UBTECH, 梅卡曼德 Mech-Mind, 速腾聚创 RoboSense, 元戎启行 DeepRoute, 星海图 Galaxea, 傅利叶 Fourier, 普渡 Pudu. Pay published as 月薪 keeps its real period (salary_period), so a salary floor no longer silently drops Chinese roles.

飞书招聘 (Feishu Hire) is not supported and won't be: it signs its job-list requests with a ByteDance _signature and gates them behind a captcha SDK, so its listings are not publicly readable. We don't break anti-bot measures.

How do I get a company added? Open a Company inclusion request issue (title it with the company + its ATS URL) — this is the best way to contribute. If you code, add it to src/openhire/seed/candidates.py (company slug + ATS vendor/tenant) and open a PR; the seeder validates tenants against the live API.

License

MIT © OpenHire Protocol · PRs welcome.


Built by a deep-tech headhunter who does not write code, pair-programming with Claude Code. Full acceptance reports, including the mistakes, in reports/.

Available Tools

5 tools
authorize_applicationAuthorize applicationA

Record an authorized, employer-direct application. REFUSES résumés.

(Formerly apply — renamed to make explicit that this only records the user's authorization to apply as themselves; it never submits anything on their behalf.)

This tool never accepts a résumé, file, cover letter, name, email or phone — a résumé never transits the server. It only takes a job_id, an anonymous fingerprint, and an explicit per-job authorization. On success it returns the apply_channel (the employer's own application URL) for the user to submit as themselves, plus resume_transmitted=false. Do NOT paste résumé content into any argument.

Args: job_id: the job to apply to (from search_jobs / check_watches). fingerprint: the user's anonymous fingerprint. authorized: must be true — explicit per-job consent.

ParametersJSON Schema
NameRequiredDescriptionDefault
job_idYes
authorizedYes
fingerprintYes

TDQS

A4.9/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only declare the write/no-destroy/idempotent-false profile; the description adds substantive behavior beyond them: no résumé ever transits the server, the call records rather than submits, and it returns apply_channel plus resume_transmitted=false. That is real operational context an agent cannot infer from the schema or annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The refusal and no-submission constraints are front-loaded, which is correct, and most sentences earn their place. The rename parenthetical is useful for disambiguation but adds length, and the résumé warning is repeated in three phrasings, so it is slightly heavier than necessary.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

There is no output schema, yet the description covers the return payload (apply_channel URL and resume_transmitted=false), the mutation semantics, and the argument constraints. Nothing needed to call this correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description carries the full burden — and it does: job_id's source is named, fingerprint is characterized as anonymous, and authorized is documented as required-true per-job consent. All three parameters gain meaning beyond their bare titles.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description gives a precise verb-and-resource ('Record an authorized, employer-direct application'), and explicitly carves out what it is NOT ('never submits anything on their behalf'), including the parenthetical history of the rename from `apply`. An agent can distinguish this from siblings like search_jobs or watch_intent immediately.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It states the trigger (recording the user's per-job authorization), the exclusions ('REFUSES résumés... Do NOT paste résumé content into any argument'), and points to where job_id comes from (search_jobs / check_watches). The when-not guidance is unusually explicit.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

check_watchesCheck watchesA

Pull matches that are new since this fingerprint's last check.

stdio has no server push, so clients pull: call this at the start of a session. Returns the new matches per watch and advances each watch's last-notified marker.

ParametersJSON Schema
NameRequiredDescriptionDefault
fingerprintYes

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare readOnlyHint=false, idempotentHint=false, destructiveHint=false, and the description explains exactly why: it 'advances each watch's last-notified marker,' a state mutation that makes repeat calls non-idempotent. That is genuine added value beyond the annotation flags. It omits auth/permission requirements and rate/volume limits.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three short sentences, each doing distinct work: what is returned, why the pull model exists, and the notification side effect. The side effect is placed last as the consequence of the call, and nothing is redundant.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description carries the return contract ('new matches per watch'), and it discloses the cursor advancement. The remaining gap is the fingerprint lifecycle – how a client obtains or resets one – which an agent may need on a first call.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0% for the single required 'fingerprint' parameter, so the description must carry the load. It does clarify that the fingerprint is the key used to look up 'this fingerprint's last check,' implying a per-client cursor identity, but it never says where the fingerprint comes from or its format.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description gives a specific verb and resource with scope: 'Pull matches that are new since this fingerprint's last check.' It is clearly the read side of the watch mechanism, which implicitly separates it from the sibling watch_intent, but it never names that sibling or any other tool to sharpen the distinction.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

'Call this at the start of a session' gives explicit, actionable timing guidance. It does not state when NOT to call it or name alternatives (e.g., watch_intent), so it falls short of the full when/when-not/alternative standard.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_company_infoCompany trust signalsA
Read-onlyIdempotent

Aggregate, anonymous trust signals for one employer.

Returns ghost_score_avg, active_jobs, and index_built_at (when the index was last built). NEVER returns any individual candidate data — the server holds none.

ParametersJSON Schema
NameRequiredDescriptionDefault
company_idYes

TDQS

A3.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnly, idempotent, and closed-world behavior, so the safety profile is covered. The description adds real value beyond that: it states the data is anonymous/aggregate and that it 'NEVER returns any individual candidate data', plus what the index build timestamp signifies. This is useful privacy-scope disclosure that annotations cannot express.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three short sentences, front-loaded with the payload summary followed by return fields and the privacy caveat. Tight and efficient, with only mild redundancy in 'the server holds none'.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With no output schema, the description compensates by naming the returned fields and their meaning (including when index_built_at was produced), which is what an agent needs. The remaining gap is the undocumented company_id parameter, which is minor given the low-complexity, single-parameter, read-only nature of the tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0% and there is a required company_id parameter that the description never explains (format, source, or expected value). The phrase 'for one employer' only loosely implies the parameter identifies the employer, leaving its semantics largely undocumented.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description gives a specific resource and action ('aggregate, anonymous trust signals for one employer') and even enumerates the returned fields (ghost_score_avg, active_jobs, index_built_at), so an agent knows exactly what it gets. It is clearly distinct from the siblings (check_watches, search_jobs, watch_intent, authorize_application), none of which concern employer trust data.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is no statement of when to call this tool, when not to, or which sibling to prefer for an adjacent need. The use case is only inferable from the purpose sentence, with no explicit routing guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

search_jobsSearch jobsA
Read-onlyIdempotent

Search the live job index by hard filters; returns ranked JobPosting[].

The server does ONLY a hard filter plus a fixed ranking of match-quality × freshness — precise re-ranking is left to you, the client, which holds the user's context. Every result includes the five protocol fields (verified_at, source, ghost_score, response_sla_days, apply_channel) plus datePosted, days_open, remote_scope, eligible_regions and role_group.

Two things worth knowing before you spend your budget:

  • role_group is shared by the same role posted in several cities — one employer may list one job 22 times, once per location. Those rows are genuinely distinct (each has its own job_id and apply_channel, which matters when the user has a location or visa constraint), but if you only need distinct opportunities, group by role_group and keep one per group. Measured, about 20% of a page is same-role repeats.

  • offset pages through the ranked list. After collapsing by role_group, call again with offset += limit to get more distinct roles. Fewer rows than limit means you reached the end. There is no server-side cursor to keep alive.

  • An empty search does NOT return a bare list. It returns an object with results: [] plus hint, unknown_skills and suggestions, because [] alone cannot tell you whether you mistyped a tag or the market is genuinely dry. Read unknown_skills: if it is non-empty those tags exist nowhere in the index and you should retry with a suggestion; if it is empty your tags were fine and you should loosen a filter.

Args: skills: skill tags, ANY-overlap match (union), e.g. ["rust", "k8s"]. required_skills: skills that must ALL be present (AND), e.g. ["rust"]. remote: if true, only fully-remote roles. remote_scope: filter remote roles by reach: "worldwide" | "region_locked" | "country_locked". min_salary: salary floor. By default roles with NO stated pay are KEPT (they can't be ruled out); set require_stated_salary=true to drop them. currency: restrict to a stated-pay currency, e.g. "USD" (implies stated pay). require_stated_salary: if true, drop roles that publish no salary. role_family: coarse family filter, e.g. "engineering". Populated for ~99% of live rows, so this is an effective way to keep sales / solutions-architect roles out of an engineering search. limit: max results (default 20).

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNo
offsetNo
remoteNo
skillsNo
currencyNo
min_salaryNo
role_familyNo
remote_scopeNo
required_skillsNo
require_stated_salaryNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
resultYes

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover the safety profile (readOnly, idempotent, non-destructive, closed-world), yet the description adds substantial behavioral context beyond them: the fixed ranking formula, the ~20% same-role duplication rate, pagination stopping semantics with no server-side cursor, and the non-obvious empty-search return shape. This is exactly the kind of behavior an agent cannot infer from annotations or schema alone.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the core action in the first sentence and organizes the rest into a bulleted 'worth knowing' section, so it is scannable. Minor flaws: it announces 'Two things worth knowing' but then lists three bullets, and the stated-pay logic is re-explained three times (min_salary, currency, require_stated_salary).

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With an output schema present, return values needn't be described, and the description wisely focuses on the non-schema-ed return nuances (the results/hint/unknown_skills/suggestions object for empty searches) plus pagination and grouping strategy. For a 10-param, zero-coverage, zero-required, read-only search tool, everything an agent needs to invoke it correctly is present.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description carries the full burden and does so thoroughly, documenting essentially all 10 parameters: skills as ANY-overlap union, required_skills as AND, remote and remote_scope (with its enum values), min_salary with its default keep-unstated-pay behavior, currency, require_stated_salary, role_family with its ~99% population note, limit, and offset. It even explains how currency and require_stated_salary interact with unstated pay.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Opens with a specific verb+resource+return type: 'Search the live job index by hard filters; returns ranked JobPosting[]'. It also clarifies the exact scope of the operation ('ONLY a hard filter plus a fixed ranking'), so the agent knows precisely what this tool does versus the other sibling tools, which operate on entirely different resources (company info, watches, applications).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives strong procedural guidance: re-ranking is delegated to the client, group by role_group to collapse same-role repeats, page with offset += limit, and a concrete decision rule for empty results ('read unknown_skills... retry with a suggestion... otherwise loosen a filter'). It does not name an alternative sibling or state explicit when-not-to-use conditions, but the siblings are unrelated tools, so the omission is minor.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

watch_intentWatch a job intentA

Register a standing intent so new matches can be pulled later.

The caller supplies its OWN anonymous fingerprint (e.g. "#a3f9") — the client generates and owns it; the server stores but can never recover it, so persist it client-side and pass the identical one to check_watches. Only the fingerprint and non-PII filter keys are stored — never a name, email, phone or résumé. Accepted filter keys mirror search_jobs: skills (ANY-overlap), required_skills (ALL/AND — use this to keep sales / solutions-architect roles out), remote (bool), role_family (e.g. "engineering"), min_salary (int). Returns { watch_id, status, fingerprint, fingerprint_notice }.

ParametersJSON Schema
NameRequiredDescriptionDefault
filtersYes
fingerprintYes

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations cover the safety profile (readOnlyHint=false, destructiveHint=false, openWorldHint=false, idempotentHint=false), and the description adds substantial context: the server stores an anonymous fingerprint it can never recover, only non-PII filter keys are persisted, and the return shape is listed. It omits consequences of the declared non-idempotency (e.g. duplicate watches from repeat calls) and any auth requirements, so it is strong but not exhaustive.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the core action, then the fingerprint contract, then filter keys, then the return object. Every sentence carries information, though the fingerprint paragraph is dense and slightly verbose for the two-parameter surface it documents.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

No output schema exists, so the description supplies the return fields ({ watch_id, status, fingerprint, fingerprint_notice }) and covers the nested, undocumented filters object. Nothing an agent needs to invoke or interpret the call is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0% and the nested filters object is untyped (additionalProperties=true), yet the description fully compensates: fingerprint generation, client ownership and persistence are explained, and every accepted filter key is enumerated with semantics (skills ANY-overlap vs required_skills ALL/AND, remote bool, role_family example, min_salary int). This is meaning the schema cannot convey on its own.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Register a standing intent so new matches can be pulled later'), which is distinct from the sibling check_watches (pulls matches) and search_jobs (one-off search). An agent can distinguish it from siblings without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly routes the agent to check_watches and tells it to persist and reuse the identical fingerprint there, and explains when to prefer required_skills over skills to exclude unwanted role types. It never states plainly when to choose this over search_jobs, so it stops short of full when/when-not coverage.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 1 tool updatev0.5.0
    • Changedsearch_jobs1 field changed
      • addedInput schema / properties / offset
        Added value: +{
        +  "default": 0,
        +  "title": "Offset",
        +  "type": "integer"
        +}
  2. 5 tool updatesv0.3.2
    • First observedauthorize_application
    • First observedcheck_watches
    • First observedget_company_info
    • First observedsearch_jobs
    • First observedwatch_intent

TDQS

A4.1/5.0

Scored across 5 tools

Disambiguation5/5

Each tool targets a clearly distinct action: search_jobs retrieves postings, authorize_application records consent, check_watches pulls new matches, get_company_info returns employer trust signals, and watch_intent registers a standing intent. The watch_intent vs check_watches pair is the closest overlap but the descriptions clearly separate register-vs-pull semantics.

Naming Consistency4/5

All names are snake_case with a verb-first pattern (search_jobs, authorize_application, check_watches, get_company_info, watch_intent). Minor deviations: get_company_info adds a third segment and watch_intent uses a noun-ish verb that reads less like a conventional action name than the others.

Tool Count5/5

Five tools is well-scoped for a job-search-and-apply server, covering discovery, application authorization, alerting, employer trust, and intent registration without redundancy.

Completeness4/5

The core lifecycle (search → watch → check → authorize apply) is fully covered. Gaps are minor: no fetch-single-job-by-id tool and no way to list or delete existing watches, but agents can work around these via repeated searches with offsets.

Maintenance

ActivityActive
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    C
    quality
    D
    maintenance
    An MCP server that enables AI-assisted job search workflows including job discovery, application tracking, resume evaluation, and cover letter generation, with support for multiple job sources and scheduled scraping.
    83
    25 npm
    1
    AGPL 3.0
  • A
    license
    Not graded
    quality
    A
    maintenance
    A local-first, open-source MCP server that analyzes jobs, matches your CV, tailors documents, and tracks applications — all on your machine with no data uploaded.
    AGPL 3.0
  • A
    license
    Not graded
    quality
    C
    maintenance
    An MCP server that exposes job-search and application-management capabilities to compatible AI clients, enabling discovery of vacancies, drafting of tailored application materials, and coordinated human-approved submissions.
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    MCP server that enables AI assistants to search LinkedIn for job posts, save them locally, and manage them via a React dashboard.
    -