seek-mcp
README.md
# seek-mcp
[](https://github.com/IntrepidShape/seek-mcp/actions/workflows/test.yml)
Australian job-search MCP server. **Zero dependencies** — the JSON-RPC 2.0 stdio transport
is ~80 lines of hand-rolled Bun, no SDK.
## Why not actually SEEK?
There is no SEEK job-**search** API. `developer.seek.com` is the hirer/ATS side — posting
ads, managing candidates, screening — gated behind a partner agreement and a seven-stage
onboarding. There is no read endpoint for job seekers, and no community SEEK MCP that isn't
a scraper sitting behind Cloudflare waiting to break.
So this reads **Adzuna**, which aggregates Australian job boards (including SEEK-sourced
ads) and has a free, instant, documented API. You get listings with apply-through links,
plus the part SEEK never gives you: aggregate salary and employer data.
## Why no SDK?
Because the protocol is small and the failure modes matter. Rolling the transport by hand
means every error path is explicit: a tool failure returns `isError: true` content the
model can recover from, a missing credential is a first-class message with the fix in it,
and an unknown method gets a proper JSON-RPC error instead of a crash. When something goes
wrong at 2am, there's no abstraction between the symptom and the cause.
## Setup
1. Free credentials at <https://developer.adzuna.com> (instant, ~1000 calls/month).
2. Register:
```
claude mcp add seek --scope user \
--env ADZUNA_APP_ID=your_id \
--env ADZUNA_APP_KEY=your_key \
-- bun run /path/to/seek-mcp/src/server.ts
```
3. Restart Claude Code, `/mcp` to confirm.
## Tools
| Tool | What it does |
|---|---|
| `search_jobs` | Keyword + location search with salary, contract-type, recency and radius filters. Returns apply links. |
| `job_market_stats` | Salary histogram, top hiring employers, month-by-month salary trend. |
| `count_jobs` | Just the match count — a cheap demand signal for comparing roles or suburbs. |
Sample — `count_jobs {"what": "registered nurse", "where": "Perth WA", "max_days_old": 7}`:
```
91 ads matching "registered nurse" — in Perth WA, posted in the last 7 days.
```
## Testing philosophy
`./test.sh` runs the handshake, tool listing, and — deliberately first — the failure paths:
credentials unset, unknown tool name. A checker that cannot fail is not a checker, so the
suite proves the red paths fire before any green is believed.
CI runs **without credentials** on purpose: it exercises exactly the negative half. The
live-query half runs locally with `ADZUNA_APP_ID`/`ADZUNA_APP_KEY` exported, and the suite
tells you it is NOT fully green until that has happened at least once:
```
negative path (credentials deliberately unset):
PASS initialize responds
PASS lists all three tools
PASS missing creds is an error
PASS unknown tool is an error
positive path (live Adzuna query):
PASS live count returns ads
```
Both failure-detection claims were themselves verified by breaking the server (wrong
endpoint, wrong assertion) and confirming the suite goes red — see the commit history.
## Design notes
- **stdio framing:** newline-delimited JSON-RPC; partial reads buffered until a full line.
- **Notifications** (no `id`) never get responses, per spec — including unknown ones.
- **Tool errors** are `result.isError`, not protocol errors, so the calling model can read
the message and retry with different arguments.
- **No `Date.now()` in output paths** — responses are deterministic given the API data,
which keeps the test assertions honest.
MIT.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues