mcp-adzuna
# mcp-adzuna
An MCP (Model Context Protocol) server that exposes [Adzuna](https://www.adzuna.com/)'s job search and salary
analytics APIs as tools.
This repo is **server-only, by design**: it's meant to be pulled into other AI systems/repos as a reusable
dependency, not run standalone. The client half of MCP — connecting to a server, listing its tools, and dispatching
tool calls from an LLM — is generic and already provided by whatever is hosting your agent (Claude Code, Claude
Desktop, or the MCP client library inside your own agent stack). Point that host at this server (see
[below](#using-from-another-repo)) rather than writing a bespoke client per project. A minimal demo client is
included under [`example/`](example/) purely to show the protocol working end-to-end — not as a reusable client
library for production use.
## Architecture
MCP defines three roles: a **host** app that embeds an LLM (Claude Code, Claude Desktop, or your own agent stack),
an **MCP client** living inside that host which speaks the MCP wire protocol (tool discovery, `tools/call`, JSON-RPC
framing), and an **MCP server** that exposes tools and responds to protocol messages without knowing who's calling
it. This repo is only the last one — the host and its MCP client are supplied by whoever consumes this server (see
[Using from another repo](#using-from-another-repo)).
Internally, the server is split into two layers with no knowledge of each other's domain:
```
LLM decides to call a tool
│
▼
Host app (Claude Code / Claude Desktop / your agent stack)
│ spawns `mcp-adzuna` as a subprocess, speaks MCP over stdio
▼
MCP Client (built into the host — see example/ for a minimal standalone one)
│ tools/call { name: "search_jobs", arguments: {...} }
▼
┌─────────────────────────── this repo ───────────────────────────┐
│ MCP Server (server.py: the `mcp` object) │
│ - owns the stdio loop, tool registry, JSON schemas │
│ - dispatches to the matching @mcp.tool() function │
│ │ │
│ ▼ │
│ search_jobs(country=..., what=...) │
│ │ plain Python function call, no protocol involved │
│ ▼ │
│ AdzunaAPI.search(...) (client.py) │
│ - builds query params, calls httpx, strips __CLASS__ noise │
└────────────────────────────┼─────────────────────────────────────┘
▼
Adzuna's REST API (api.adzuna.com)
```
- **`server.py`** is the only file that speaks MCP: tool names, docstrings-as-descriptions, type hints-as-JSON-schema.
- **`client.py`**'s `AdzunaAPI` is a plain REST wrapper around Adzuna's HTTP API — it has no knowledge that MCP
exists. It's deliberately *not* named `AdzunaClient`, to avoid confusion with the "MCP client" role above; it's a
client of Adzuna's API in the ordinary SDK sense (like `httpx.Client` or `boto3.client(...)`), not an MCP client.
This split means `AdzunaAPI` is independently testable (`tests/test_client.py` runs against a mocked HTTP transport
with zero MCP runtime involved) and independently reusable (importable in a plain script with no MCP dependency
dragged in).
## Tools
| Tool | Adzuna endpoint | What it does |
|---|---|---|
| `search_jobs` | `/jobs/{country}/search/{page}` | Search job listings by keyword, location, salary, category, etc. |
| `list_categories` | `/jobs/{country}/categories` | List job category tags (e.g. `it-jobs`, `sales-jobs`). |
| `salary_histogram` | `/jobs/{country}/histogram` | Distribution of salaries for a search as a histogram. |
| `top_companies` | `/jobs/{country}/top_companies` | Top 5 employers by vacancy count for a search. |
| `regional_data` | `/jobs/{country}/geodata` | Vacancy counts per sub-region of a location. |
| `historical_salary` | `/jobs/{country}/history` | Average salary by month, over time. |
| `api_version` | `/version` | Current Adzuna API version (useful for checking connectivity/auth). |
## Setup
1. Get a free `app_id`/`app_key` at [developer.adzuna.com/signup](https://developer.adzuna.com/signup).
2. Install the package:
```bash
pip install -e .
```
3. Set your credentials:
```bash
cp .env.example .env
# edit .env and fill in ADZUNA_APP_ID / ADZUNA_APP_KEY
```
## Running standalone
```bash
export ADZUNA_APP_ID=... ADZUNA_APP_KEY=...
mcp-adzuna
```
This starts the server on stdio, the standard transport for local MCP clients.
## Using from another repo
Since this package isn't published to PyPI, other repos can run it straight from GitHub with
[`uv`](https://docs.astral.sh/uv/)'s `uvx` — no local clone or install step needed in the consuming repo:
```json
{
"mcpServers": {
"adzuna": {
"command": "uvx",
"args": ["--from", "git+https://github.com/fabioba/mcp-adzuna.git", "mcp-adzuna"],
"env": {
"ADZUNA_APP_ID": "your-app-id",
"ADZUNA_APP_KEY": "your-app-key"
}
}
}
}
```
Or, with Claude Code's CLI:
```bash
claude mcp add adzuna --env ADZUNA_APP_ID=your-app-id --env ADZUNA_APP_KEY=your-app-key \
-- uvx --from git+https://github.com/fabioba/mcp-adzuna.git mcp-adzuna
```
`uv`/`uvx` will fetch and cache the package from the git repo on first run, and re-fetch when the pinned ref
changes — pin to a tag or commit (`git+https://...@v0.1.0`) once you cut a release, so consuming repos don't pick up
breaking changes silently.
If a consuming repo already manages its own Python environment, adding `mcp-adzuna @ git+https://github.com/fabioba/mcp-adzuna.git`
to its `pyproject.toml`/`requirements.txt` dependencies works the same way, and `mcp-adzuna` becomes an installed
console-script entry point in that environment's `mcpServers` config instead.
## Using locally with Claude Code / Claude Desktop
For local development on this repo itself, add it to your MCP config using the local install (see
[Setup](#setup)) instead:
```json
{
"mcpServers": {
"adzuna": {
"command": "mcp-adzuna",
"env": {
"ADZUNA_APP_ID": "your-app-id",
"ADZUNA_APP_KEY": "your-app-key"
}
}
}
}
```
## Example client
[`example/`](example/) contains a minimal MCP client that spawns this server and calls a tool, to see the protocol
work end-to-end without setting up a full MCP host first:
```bash
pip install -e ".[dev]"
python example/client.py
```
See [`example/README.md`](example/README.md) for what it demonstrates.
## Development
```bash
pip install -e ".[dev]"
pytest
```
Tests run against a mocked HTTP transport (`httpx.MockTransport`) and don't require real Adzuna credentials.
TDQS
Scored across 7 tools
Each tool targets a distinct aspect: job search, category listing, salary distribution, top companies, regional breakdown, historical salary, and API version. There is no overlap, and descriptions clearly differentiate each tool's purpose.
Tool names follow a mix of verb_noun (search_jobs, list_categories) and noun_phrase (salary_histogram, top_companies, regional_data, historical_salary, api_version) patterns. All are lowercase with underscores, but the lack of a consistent verb prefix makes the naming pattern inconsistent.
With 7 tools, the server is well-scoped for a job search and analytics domain. Each tool serves a distinct purpose without redundancy or bloat.
The server covers the core job search workflow, including filtering, category discovery, salary analysis, and regional breakdowns. However, there is no explicit tool for fetching a single job's full details or for paginating beyond setting a page size, leaving minor gaps in the lifecycle.