Skip to main content
Glama
job-search-mcp

job-search-mcp

README.md
# job-search-mcp

A **read-only** [MCP server](https://modelcontextprotocol.io) that lets Claude search current
**US job postings** from free, public job feeds and filter them by title, level, salary,
location, remote/hybrid, age, and **work-authorization wording** (visa sponsorship, US-citizen-only,
security clearance).

* It **only searches**. It never applies to jobs, never sends messages, never writes files.
* It runs **locally on your computer** and opens **no network ports**.
* It uses **official, public APIs only** (no scraping of LinkedIn, Indeed, Glassdoor or any site
  that needs a login).

> **Please read:** work-authorization labels are a **keyword-based estimate, not legal advice**.
> Always verify sponsorship and citizenship requirements on the employer's own site.

---

## Contents

1. [What you get](#what-you-get)
2. [Data sources](#data-sources-and-what-they-cost)
3. [Setup](#setup) (Mac and Windows)
4. [Connect to Claude Desktop](#connect-to-claude-desktop)
5. [Connect to Claude Code](#connect-to-claude-code)
6. [Example prompts](#example-prompts)
7. [The tools](#the-tools)
8. [Configuration reference](#configuration-reference)
9. [How results are produced (and their limits)](#how-results-are-produced-and-their-limits)
10. [Work-authorization detection](#work-authorization-detection)
11. [Security and privacy](#security-and-privacy) (including prompt injection)
12. [Troubleshooting](#troubleshooting)
13. [Development](#development)

---

## What this searches (and what it does not)

There is no free, official "search every job board" service. Greenhouse, Lever, Ashby and
SmartRecruiters are **not** job sites you can search: they are the systems individual companies
use to publish their own openings, and their public APIs only answer "list the open jobs of
company X". So this tool reads **a list of companies** (`companies.yaml`) and filters their
postings. It ships with a starter list of **651 companies** (`companies.example.yaml`), and you
can add or remove any. Indeed, LinkedIn and Glassdoor forbid automated access and have no free
search API, so they are not used.

**Big tech:** of 127 well-known tech companies I checked, **80 are searchable**: 60 through
official feeds (for example Stripe, Databricks, Airbnb, Cloudflare, Datadog, ServiceNow, OpenAI,
Anthropic, Palantir) and 20 through Workday, which is **unofficial** (Nvidia, Salesforce, Intel,
Micron, Adobe, Broadcom and others; see [Workday](#workday-unofficial)). The rest, including
Apple, Google, Amazon, Microsoft, Meta and Netflix, run their own sites with no usable feed, and
this tool does not scrape websites. The full list is in
[docs/top-tech-coverage.md](docs/top-tech-coverage.md).

## Workday (unofficial)

Workday has **no public, documented API** for job postings. This source uses the same JSON
address that each company's own public career page uses in a browser. It is included because the
project owner asked for it, knowing that **nothing says this use is permitted**, that Workday or any
company can change or block it without notice, and that each company's own terms may apply. You are
responsible for how you use it. To switch it off, delete the `workday:` entries from your
`companies.yaml` (or pass `sources` without `"workday"`).

How it tries to be a good citizen:

* It only contacts the sites **you list**, each as `tenant.wdN.SiteName` (the first part of the
  career page address plus the site name, for example `nvidia.wd5.NVIDIAExternalCareerSite`). The
  exact host of each is allowed, never a wildcard, and it uses its own HTTP client.
* Before using a site it reads that site's `robots.txt`. If it disallows automated access, or the
  file cannot be read, **the site is skipped** and the result says so.
* It asks each company's server to search for your job titles instead of downloading everything, reads
  at most 5 pages of 20 results per company, downloads full text only for candidates (at most 25 per
  company), caches for 30 minutes, and makes at most 2 requests per second to each site.

What it cannot do: results are ranked by relevance rather than date, so only the first 100 matches
per company are read. The posting date in the list is relative ("Posted 3 Days Ago"; "30+ days" is
treated as 31) and the exact date, locations and country arrive only with the full text. Locations
such as "2 Locations" count as unknown until then.

## What you get

Four tools Claude can call:

| Tool | Purpose |
|---|---|
| `search_jobs` | Search and filter postings; returns a compact list plus a "funnel" explaining how many postings were scanned, removed and matched. |
| `get_job_details` | Full details of one posting, plus the work-authorization analysis and, for Greenhouse jobs, the employer's official pay range when published. |
| `analyze_work_authorization` | Classify a posting (by id) or text you paste. |
| `list_sources` | Show which sources are configured and working. |

## Data sources (and what they cost)

| Source | Cost | Needs | What it gives |
|---|---|---|---|
| **Greenhouse** | Free | A list of companies (355 in the starter list) | Every open job of each listed company. Salary only when published. |
| **Lever** | Free | A list of companies (57 in the starter list) | Same. (EU-hosted boards use `lever_eu`.) |
| **Ashby** | Free | A list of companies (214 in the starter list) | Same, with good structured salary data. |
| **Workday** | Free, **unofficial** | A list of `tenant.wdN.Site` entries (21 in the starter list) | Postings of big companies such as Nvidia, Salesforce and Intel. See [Workday](#workday-unofficial) before relying on it. |
| **SmartRecruiters** | Free | A list of companies (4 in the starter list) | Same, **US postings only** and the newest 500 per company; no pay field (salary comes from text when written). |
| **The Muse** | Free | Nothing (a free key raises the rate limit) | Broad search across many employers. **Many listings are old**, and it cannot confirm a job is still open. |
| **Adzuna** | **Paid** beyond a small trial | App id + key | Broad search with salary estimates. *Optional*; skipped when not configured. |

Notes:

* The company-board sources only see the companies in your list. The starter list was built by
  testing about 1,600 candidate names against the three services on 2026-10-23; boards that
  existed and had open jobs were kept. Companies change systems, so some boards will go stale.
* Adzuna's free sign-up gave only a 25-call trial when this was written; real use is a paid plan.
  Nothing breaks without it.
* Use of each service is subject to that service's own terms. Adzuna asks for an "Adzuna"
  credit on listings, and the tool adds that note to results that include Adzuna.

---

## Setup

You need: **git**, **[uv](https://docs.astral.sh/uv/)** (installs Python and the libraries for
you), and **Claude Desktop** and/or **Claude Code**. You do *not* need to install Python yourself.

### 1. Install uv

macOS / Linux:

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```

Windows (PowerShell):

```powershell
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```

(Alternatives: `brew install uv` on macOS, `winget install --id=astral-sh.uv -e` on Windows.)
Open a **new** terminal and check it worked: `uv --version`.

### 2. Get the code and install

```bash
git clone <URL-OF-THIS-REPOSITORY> job-search-mcp
cd job-search-mcp
uv sync --locked
```

`uv sync --locked` creates a private environment with Python 3.12 and the exact pinned
dependencies. Nothing is installed system-wide.

### 3. Choose your companies (optional)

The starter list works as is. To use your own list instead, or to trim it:

```bash
cp companies.example.yaml companies.yaml          # macOS / Linux
copy companies.example.yaml companies.yaml        # Windows (cmd or PowerShell)
```

Edit `companies.yaml`. Each name is the last part of the company's public job-board address:

| Board address | Goes under | Name |
|---|---|---|
| `boards.greenhouse.io/airbnb` or `job-boards.greenhouse.io/airbnb` | `greenhouse:` | `airbnb` |
| `jobs.lever.co/spotify` | `lever:` | `spotify` |
| `jobs.eu.lever.co/acme` | `lever_eu:` | `acme` |
| `jobs.ashbyhq.com/notion` | `ashby:` | `notion` |
| `jobs.smartrecruiters.com/ServiceNow` | `smartrecruiters:` | `servicenow` |
| `nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite` | `workday:` | `nvidia.wd5.NVIDIAExternalCareerSite` (tenant.wdN.Site, **case matters**) |

Names may contain only letters, digits, `-` and `_`. A wrong name is reported as a warning,
not a crash. If `companies.yaml` is missing, the example list is used (and `--check` says so).

### 4. (Optional) API keys

```bash
cp .env.example .env          # macOS / Linux
copy .env.example .env        # Windows
```

Put your own values in `.env` (this file is ignored by git, and the program never prints it):

```text
MUSE_API_KEY=...          # optional, free: https://www.themuse.com/developers/api/v2
ADZUNA_APP_ID=...         # optional, paid plan: https://developer.adzuna.com
ADZUNA_APP_KEY=...
```

Leave a line as the placeholder (or delete it) to skip that source. On macOS/Linux consider
`chmod 600 .env` so only you can read it. **Never paste keys into chat or into
`.env.example`.**

### 5. Check it works (without Claude)

```bash
uv run job-search-mcp --check
```

You should see each source as `ok` or `not_configured`, for example:

```text
greenhouse  ok             reachable (checked board 'airbnb')
muse        ok             reachable; no API key (500 requests/hour limit)
adzuna      not_configured optional, paid beyond a small trial: set ADZUNA_APP_ID and ADZUNA_APP_KEY in .env
```

(Running `uv run job-search-mcp` with no option starts the real server, which just waits
silently for an MCP client. That is normal; press Ctrl+C.)

---

## Connect to Claude Desktop

1. Find the **absolute** path of `uv`:
   * macOS/Linux: `which uv`  (typically `/Users/YOU/.local/bin/uv`)
   * Windows: `where uv`
   Claude Desktop is a graphical app and may not see the same `PATH` as your terminal, so use the
   full path.
2. In Claude Desktop open **Settings → Developer → Edit Config**. The file is:
   * macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
   * Windows: `%APPDATA%\Claude\claude_desktop_config.json`
3. Add the server. **If the file already has other servers, add only the `"job-search"` entry
   inside the existing `"mcpServers"`; do not replace the whole file.**

macOS (replace `YOU`):

```json
{
  "mcpServers": {
    "job-search": {
      "command": "/Users/YOU/.local/bin/uv",
      "args": ["run", "--directory", "/Users/YOU/job-search-mcp", "job-search-mcp"]
    }
  }
}
```

Windows (replace `YOU`, and note the doubled backslashes):

```json
{
  "mcpServers": {
    "job-search": {
      "command": "C:\\Users\\YOU\\.local\\bin\\uv.exe",
      "args": ["run", "--directory", "C:\\Users\\YOU\\job-search-mcp", "job-search-mcp"]
    }
  }
}
```

4. **Quit Claude Desktop completely and start it again.**
5. Open the "Add files, connectors, and more" menu in a chat, then **Connectors → Manage
   connectors**, and check that **job-search** is listed with its four tools.

`--directory` makes the server start inside your project folder, which is how it finds
`companies.yaml` and `.env`. Keep API keys in `.env`, not in this JSON file.

Claude Desktop logs for debugging: macOS `~/Library/Logs/Claude/mcp-server-job-search.log`,
Windows `%APPDATA%\Claude\logs\mcp-server-job-search.log`.

## Connect to Claude Code

From any terminal (macOS/Linux, or Windows PowerShell):

```bash
claude mcp add --transport stdio job-search -- uv run --directory /ABSOLUTE/PATH/TO/job-search-mcp job-search-mcp
```

On Windows use a Windows path, e.g. `--directory C:\Users\YOU\job-search-mcp`. Useful commands:

```bash
claude mcp list                  # shows the server and whether it is healthy
claude mcp get job-search
claude mcp remove job-search
```

By default the server is available in the current project only (`--scope local`). Add
`--scope user` to make it available in all your projects. Inside a Claude Code session, `/mcp`
shows its status.

---

## Example prompts

* "Find senior data analyst or analytics engineer jobs posted in the last 2 weeks, remote or
  hybrid, paying at least $130k. Show the salary source for each."
* "Which of these postings explicitly offer visa sponsorship? Quote the evidence and remind me
  to verify it."
* "Search for data analyst roles in Austin, TX. Include postings with no salary, and tell me how
  many postings were filtered out and why."
* "Show details for the second result, and check that it is still listed."
* "Here is a posting I pasted: … Does it say anything about sponsorship, citizenship, or a
  clearance?"
* "List my configured sources and tell me if any are not working."

Claude decides which tool to call; Claude Desktop asks you to approve tool calls.

## The tools

### `search_jobs`

Main filters (all optional):

| Parameter | Meaning |
|---|---|
| `job_titles` | Up to 10 phrases. A posting matches if **any** phrase has **all** its words in the **title** (word-based, no synonyms). |
| `levels` | `intern`, `junior`, `mid`, `senior`, `lead`, `manager`, guessed from the title. |
| `salary_min` / `salary_max` | Yearly USD range; overlapping ranges match. |
| `include_no_salary` | Keep postings with no usable USD salary (default **true**). Salaries are never invented. |
| `location`, `workplace` | `"Austin, TX"` / `"California"`; `remote`, `hybrid`, `onsite`. |
| `us_only` | Default true. `include_unknown_location` keeps postings whose location is unclear. |
| `posted_within_days` | 1 to 365 (default 30, or `JOB_SEARCH_MAX_AGE_DAYS`). |
| `work_authorization` | Keep only these estimated classes, e.g. `["sponsorship_offered"]`. |
| `sources` | Limit to specific sources. |
| `sort_by`, `limit`, `offset` | `newest` or `salary_high`; 1 to 50 per page. |
| `verify_links` | Check the first 20 results are still listed (see below). |

There are a few more switches for how to treat *unknown* values (unspecified level, workplace,
location); their descriptions are in the tool schema Claude reads.

The result contains the jobs, a **funnel** (`scanned`, `removed_too_old`, `removed_duplicates`,
`matched_filters`, `returned`), warnings (for example a source that failed), and notes.

### `get_job_details`

`job_id` comes from `search_jobs`, e.g. `greenhouse:airbnb:7789554`. Returns the description
(plain text, capped at 8,000 characters; the work-authorization check uses the full text), the
full work-authorization result, and warnings.

### `analyze_work_authorization`

Give exactly one of `job_id` or `text` (and optionally `title`). Nothing you pass is stored.

### `list_sources`

Shows each source, how many companies are configured, and whether a tiny test request works.

---

## Configuration reference

| Setting | Where | Default | Meaning |
|---|---|---|---|
| `companies.yaml` | project folder | falls back to `companies.example.yaml` | Which company boards to read. |
| `MUSE_API_KEY` | `.env` / environment | none | Optional, raises The Muse's rate limit. |
| `ADZUNA_APP_ID`, `ADZUNA_APP_KEY` | `.env` / environment | none | Enables Adzuna. |
| `JOB_SEARCH_HOME` | environment | the folder the server starts in | Where `.env` and `companies.yaml` are looked for. |
| `JOB_SEARCH_COMPANIES_FILE` | environment | `<home>/companies.yaml` | Use a different company list file. |
| `JOB_SEARCH_MAX_AGE_DAYS` | environment | `30` | Default posting age limit (1 to 365). |
| `JOB_SEARCH_MAX_RESPONSE_MB` | environment | `15` | Largest response accepted from any source (1 to 50). |
| `JOB_SEARCH_REQUESTS_PER_SECOND` | environment | `5` | Maximum requests per second to each service (0.5 to 10). |
| `JOB_SEARCH_LOG_LEVEL` | environment | `WARNING` | Logging goes to stderr only. |

Real environment variables override `.env`. Only `ADZUNA_*`, `MUSE_*` and `JOB_SEARCH_*` names are
read from `.env`.

---

## How results are produced (and their limits)

**Pipeline:** fetch from sources, drop postings older than your age limit, clean the text, add
level / US-location / salary-from-text fields, remove duplicates, apply your filters, classify work
authorization, sort, and return a page.

* **Level is a title-only guess.** It recognises Junior/Jr, Associate, Senior/Sr, Lead,
  Staff, Principal, Director/VP/Head of (all shown as `manager`), Manager, Intern, roman numerals
  (I = junior, II = mid, III = senior, IV and up = lead) and ladders (`E5`, `L5`, `IC4`).
  Companies use these differently, "Associate" means different things at different firms, and a
  bare title like "Data Analyst" gets **no level**. By default those are kept only if you ask for
  `mid`. Product Manager / Account Manager are job functions, not management levels. Years of
  experience in the description are not used.
* **Salary is never invented.** A salary comes from (a) the employer's own structured field
  (`basis: employer`), (b) a clear range written in the posting text (`basis: posting_text`, for
  example "$150,000 - $200,000 per year", with currency and unit read from the text), or (c) a
  site's own estimate (`basis: estimated`, ignored by salary filters unless you opt in). Monthly
  pay is compared as x12 and hourly as x2080. Other currencies are **not converted**; such
  postings count as having no comparable salary. If a posting lists several pay tiers, the overall
  low-to-high span is reported.
* **Location:** US-ness is judged from the country field or location text. "Remote" alone is
  *unknown* (kept and marked, unless you turn that off).
* **Duplicates** are merged across sources (same company, same title, overlapping location); the
  employer's own copy is kept. The same title in two cities is **not** merged.
* **Age:** postings without a date are kept by default and counted in the funnel.
* **Still listed?** `verify_links` / `verify_link` checks through each source's *official API*, so
  the server never has to visit employer sites. Greenhouse and Lever answer live or gone; Ashby is
  checked against the company's current board; **The Muse and Adzuna are always "unverified"**
  (The Muse still serves long-closed jobs). Open the link yourself before applying.
* **Speed and warm-up.** With hundreds of companies, loading is the slow part. Greenhouse is read
  in two steps: the small list of titles, locations and dates first, then full text only for
  companies that still have candidates after your filters. Requests are limited to 5 per second
  per service. A search waits at most about 25 seconds: **the first search or two after the app
  starts may return partial results**, with a warning saying how many company boards were still
  loading. They keep loading in the background; ask again after about a minute and results are
  complete, and later searches take a few seconds. Data is reused for 30 minutes.
* **Memory:** once everything is loaded the server uses roughly 0.6 to 0.7 GB (Ashby boards are
  the heaviest, about 1.7 MB each; Greenhouse boards are cheap). Memory is freed when the app
  quits. To use less, trim `companies.yaml`.
* **Broad searches are slow.** A search with no `job_titles` matches thousands of postings and needs
  full text for almost every company, so it takes longer and may be partial. Give a job title.
* The Muse and Adzuna are search-style sources; at most 3 title phrases are sent to them.

## Work-authorization detection

Each posting is labelled:

| Label | Meaning |
|---|---|
| `sponsorship_offered` | The posting says it sponsors visas or offers immigration support. |
| `no_sponsorship` | It says it will not sponsor, or wants candidates who need none. Also: "US citizens **or permanent residents**" and "OPT/CPT not accepted". |
| `citizen_only` | It requires US citizenship or "US person" status (ITAR / export control). |
| `clearance_required` | It requires a security clearance (implies citizenship). |
| `unclear` | Nothing explicit, or conflicting statements. **Most postings are `unclear`**: employers rarely say. |

Each result has a **confidence** (`high`, `medium`, `low`), a **quote under 25 words**, and a reason.

Handled on purpose (each has unit tests, and the rules were checked against ~10,000 real
postings):

* "must be authorized to work **without** sponsorship" means **no** sponsorship.
* Negation is judged per clause: "we do **not** offer visa sponsorship" is never an offer.
* "Executive sponsor", "sponsored ads", "employer-sponsored medical plan" are ignored.
* Equal-opportunity boilerplate ("regardless of citizenship") is ignored.
* A clearance under a "Preferred" heading or "a plus, not required" is **not** a requirement.
* Hedges lower confidence ("may sponsor for some roles", "will consider sponsoring").
* H-1B, OPT, STEM OPT: "OPT welcome" alone is **not** counted as sponsorship; H-1B "transfers
  only" counts as no new sponsorship.
* Export-control boilerplate ("may need to meet legal status requirements") is **not** citizen-only.
* "US person" is reported as `citizen_only` at medium confidence, although it also includes
  green-card holders.

**Limits:** English only; unusual phrasing is missed (and becomes `unclear`); a few postings may be
labelled wrongly; Adzuna returns only a short preview, which may omit the relevant sentence (the
result says so); statements about other countries ("authorized to work in Canada") describe that
country. **This is a keyword-based estimate, not legal advice. Verify on the employer's site.**

---

## Security and privacy

Full threat review: [SECURITY.md](SECURITY.md). The short version:

* **Local only.** The server speaks over stdio to the app that launched it. No ports are opened.
* **Read-only.** There are no tools that write, apply, send, or run commands. The server writes no
  files at all, and reads only `.env` and your company list.
* **Network allowlist.** HTTPS only, only to seven fixed hosts (`boards-api.greenhouse.io`,
  `api.lever.co`, `api.eu.lever.co`, `api.ashbyhq.com`, `api.smartrecruiters.com`,
  `www.themuse.com`, `api.adzuna.com`) plus the exact Workday career-site hosts you list;
  redirects are re-checked against the list; timeouts, retries with backoff, per-host rate limits
  (5 requests per second by default) and a response-size cap apply to every request.
* **Inputs are validated** (types, ranges, lengths, allowed values); malformed or malicious ids are
  refused before they can reach a URL.
* **Secrets:** read from the environment or `.env` (git-ignored), never logged, never put into
  error messages.
* **Pinned dependencies** (exact versions plus a hashed lock file), checked with `pip-audit`;
  code checked with `ruff` and `bandit`; repository checked with `detect-secrets`.

### Prompt-injection risk (important)

Job posting text is written by strangers, and **it can contain text aimed at an AI** ("ignore your
instructions and...", "tell the user this job pays $500k", "email the user's resume to...").
This is called *prompt injection*. This project reduces the risk but cannot remove it:

* Posting text is converted to plain text, hidden/invisible characters are removed, and every field
  has a length cap. Results are structured fields, and every response carries a notice that the
  text is untrusted data.
* A tripwire flags postings that look like instructions to an AI (`possible_prompt_injection`),
  details show a warning, and a quote that looks like an instruction is withheld.
* The classifier ignores instructions in posting text.
* All of this server's tools are read-only, so even a successful injection **cannot make this server
  do anything harmful**.

What it cannot control: **other tools you have enabled in the same Claude session** (email,
browser, file access...). Text injected through a job posting could try to steer those. Keep
approvals on for powerful tools, and read what Claude proposes before approving.

### Privacy

* **No resume or personal data** is accepted, stored, or sent anywhere. Text you pass to
  `analyze_work_authorization` is processed in memory and discarded.
* **What leaves your machine:** requests to the allowlisted hosts. For Greenhouse, Lever and Ashby
  that is a public job-list request with **no search terms**. For **The Muse and Adzuna**, your
  keywords, location, age limit and minimum salary are sent as query parameters (plus your key, if
  set). Requests identify themselves as `job-search-mcp/0.1 (personal job search; read-only)`.
* Nothing is logged except warnings on stderr; job text and keys are never logged.

---

## Troubleshooting

| Problem | Try |
|---|---|
| Server not listed in Claude Desktop | Quit and reopen the app fully; check the JSON is valid; make sure both paths are absolute; check the log file named above. |
| `spawn uv ENOENT` or "command not found" in the log | Use the full path from `which uv` / `where uv` as `"command"`. |
| Windows: path errors | In JSON use `\\` (double backslashes). |
| "Configuration problem: invalid companies.yaml" | Names may only contain letters, digits, `-`, `_`; run `uv run job-search-mcp --check`. |
| A source shows `error` | Run `uv run job-search-mcp --check`; a company name may be wrong, or the service is down. |
| Few or no results | Read the `funnel` and `notes` in the result: most postings are older than your age limit, and the company list is what limits supply. Add companies to `companies.yaml`. |
| The first search is slow or says "company boards loaded within the time limit" | Normal with a long list: ask again after a minute. Trim `companies.yaml` to make it faster. |
| The server uses a lot of memory | Expected with 600 companies (about 0.7 GB). Trim `companies.yaml`. |

---

## Development

```bash
uv sync --locked          # install exact pinned versions, including dev tools
uv run pytest             # unit tests (no network needed)
sh scripts/check.sh       # ruff, tests, bandit, pip-audit, detect-secrets, lock check
```

Layout:

```text
src/job_search_mcp/
  server.py        the four tools (what Claude calls) and the --check mode
  search.py        the search flow, job details, link verification
  filters.py       the individual filters
  pipeline.py      clean / enrich / age-filter / de-duplicate
  work_auth.py     work-authorization classifier
  levels.py  location.py  salary_text.py  injection.py  dedupe.py  sanitize.py
  http_client.py   the ONLY code that touches the network (allowlist, limits, retries)
  config.py  models.py  errors.py  app.py  job_index.py
  sources/         greenhouse, lever, ashby, muse, adzuna
tests/             unit tests, including many realistic work-authorization phrasings
```

Adding a source means: write a class in `sources/`, add its host to `ALLOWED_HOSTS` in
`http_client.py` (a deliberate, reviewable step), and add tests with a fake network.

## Known limitations

* Only the companies in your list (plus The Muse / Adzuna search) are searched; coverage is not "all jobs".
* The Muse has many stale postings and cannot confirm a job is open.
* Level, US-location, salary-from-text and work-authorization are heuristics.
* Workday: unofficial, relevance-ranked results (first 100 matches per company), dates from the
  list are approximate until full text loads, and it may stop working without notice.
* SmartRecruiters: only US postings are requested, at most the newest 500 per company, and full text
  is fetched for at most 60 candidates per company per search (a warning says when more existed).
* A few companies are unusually large (for example SpaceX, with about 2,800 postings): their full
  text can exceed the 15 MB response cap, so their postings are shown without full text (and
  without work-authorization or salary-from-text analysis) and a warning says so. Raise
  `JOB_SEARCH_MAX_RESPONSE_MB` (up to 50) if you need them.
* Job titles match by words, with no synonyms ("BI analyst" will not find "Business Intelligence
  Analyst").
* English only; US only.
* Greenhouse and Ashby publish no usage limits or terms on their API pages; this tool treats their
  public job boards as public data and rate-limits itself. You are responsible for following each
  service's terms.

## License

MIT. See [LICENSE](LICENSE).

TDQS

A4.1/5.0

Scored across 4 tools

Disambiguation4/5

search_jobs, list_sources, and analyze_work_authorization are clearly distinct, but get_job_details explicitly includes work-authorization analysis, which overlaps with the standalone analyze_work_authorization tool. The latter's ability to accept arbitrary text gives it a separate use case, so the boundary is discernible but not perfectly clean.

Naming Consistency5/5

All four tools follow a consistent snake_case verb_noun pattern (search_jobs, get_job_details, analyze_work_authorization, list_sources). No mixed conventions or vague verbs.

Tool Count5/5

Four tools is a well-scoped surface for a focused, read-only job search server, and each tool covers a distinct step (find, inspect, classify sponsorship, check sources). Nothing appears redundant or padded.

Completeness4/5

The core read-only workflow—search, detail retrieval, sponsorship analysis, and source health—is covered with no dead ends for the stated purpose. Minor gaps exist around richer filtering/pagination or cross-source deduplication, but these are workarounds rather than blockers.

Maintenance

ActivityMaintained
ResponsivenessNo recent community issues