Skip to main content
Glama
IntrepidShape

seek-mcp

seek-mcp

test

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
  1. 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.