Skip to main content
Glama
chinpeerapat

JobSpy MCP Server

by chinpeerapat
README.md
# JobSpy MCP Server

An MCP (Model Context Protocol) server that scrapes live job postings from LinkedIn, Indeed, Glassdoor, ZipRecruiter, Google Jobs, Bayt, Naukri, and BDJobs using [python-jobspy](https://github.com/speedyapply/JobSpy). Built on [FastMCP 3.x](https://gofastmcp.com) against the current MCP spec (2025-11-25).

## Why local?

Job boards aggressively block datacenter IPs, and JobSpy depends on `tls-client` (a native binary). Running the server on your own machine over stdio gives the best scrape success rate. Streamable HTTP mode is available behind a flag if you want to host it (bring your own proxies).

## Installation

### Claude Desktop (MCPB, one-click)

Download [`jobspy-mcp-server.mcpb`](https://github.com/chinpeerapat/jobspy-mcp-server/releases/download/mcpb-latest/jobspy-mcp-server.mcpb) (rolling build from `main`) and open it with Claude Desktop, or drag the file onto the app. Versioned files live on [Releases](https://github.com/chinpeerapat/jobspy-mcp-server/releases). The host's UV runtime installs Python and dependencies on first launch — you do not need a local Python toolchain.

Optional install-time settings: proxies and a CA certificate bundle (for proxied HTTPS). See [PRIVACY.md](PRIVACY.md).

CI packs and publishes the bundle on every push to `main`. To pack locally:

```bash
bash scripts/build-mcpb.sh
# → dist/jobspy-mcp-server-<version>.mcpb
# → dist/jobspy-mcp-server.mcpb
```

### From source (stdio)

Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).

```bash
git clone <this-repo> && cd jobspy-mcp-server
uv sync
```

Note: `python-jobspy` is pinned to a GitHub commit ahead of the PyPI release (which is missing LinkedIn/Naukri fixes), so `git` must be on your PATH during install.

## Usage

### Claude Desktop (manual stdio)

If you are not using the MCPB, add this to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%/Claude/claude_desktop_config.json` (Windows):

```json
{
  "mcpServers": {
    "jobspy": {
      "command": "uv",
      "args": ["run", "--directory", "/absolute/path/to/jobspy-mcp-server", "jobspy-mcp-server"]
    }
  }
}
```

Tip: Claude Desktop launches with a limited `PATH` that usually excludes `~/.local/bin`, so you may need the absolute path to `uv` (e.g. `/Users/you/.local/bin/uv`) in `command`. (Thanks to @VennieSo in [#1](https://github.com/chinpeerapat/jobspy-mcp-server/pull/1).)

### Cursor / other MCP clients

Any stdio-capable client works with the same command:

```bash
uv run --directory /absolute/path/to/jobspy-mcp-server jobspy-mcp-server
```

### HTTP mode (optional)

```bash
uv run jobspy-mcp-server --http --port 8000
# MCP endpoint: http://127.0.0.1:8000/mcp
```

Set `JOBSPY_HTTP_TOKEN` to require a bearer token; without it the endpoint is unauthenticated and should stay on localhost.

#### `--stateless` (HTTP only, opt-in)

By default HTTP mode keeps a short-lived session per client in memory. That is the right default for one process.

If you run more than one process — several workers, or a load balancer in front of several containers — those sessions live on whichever process handled the first request. The next request can land on a different process and fail.

`--stateless` turns sessions off. Each request is handled on its own, so any process can take the next call. Job searches already send everything they need in the request (page with `offset`), so this server does not depend on sessions.

```bash
uv run jobspy-mcp-server --http --stateless --port 8000
```

Notes:

- Requires `--http`. Stdio is unchanged, and `--stateless` without `--http` is an error.
- A single `--http` process does not need it.
- Progress heartbeats during a scrape still work; they go out on that same request, not via a stored session.
- Do not use this as a reason to run the scraper serverless. Job boards block datacenter IPs, and JobSpy needs a native TLS library.

## Environment variables

| Variable | Purpose |
|---|---|
| `JOBSPY_PROXIES` | Comma-separated proxy list passed to JobSpy (useful for hosted mode or heavy scraping) |
| `JOBSPY_CA_CERT` | Path to a CA certificate bundle for proxied HTTPS |
| `JOBSPY_HTTP_TOKEN` | Bearer token required by HTTP mode when set |

## What it exposes

### Tool: `search_jobs`

Read-only search across the supported boards. Key parameters:

| Parameter | Notes |
|---|---|
| `search_term` | Required keywords |
| `sites` | Boards to scrape; default `["indeed"]` (most reliable) |
| `location`, `distance`, `is_remote` | Where |
| `results_wanted` | Per site, capped at 50 |
| `job_type` | fulltime / parttime / contract / temporary / internship |
| `hours_old` | Only postings from the last N hours |
| `offset` | Pagination |
| `country_indeed` | Country for Indeed/Glassdoor (see `jobspy://countries`) |
| `google_search_term` | Required when scraping Google Jobs |
| `include_description` | Truncated descriptions, off by default (keeps responses small) |

Returns compact JSON records (title, company, location, salary, posted date, URL) as both text and structured content.

### Resources

- `jobspy://sites` — per-board capabilities and quirks
- `jobspy://countries` — valid `country_indeed` values

### Prompt

- `job-search` — a canned multi-board search workflow (`role`, optional `location`)

## What changed in 2.0

Version 2.0 is a ground-up rewrite of the 1.x server.

### Changes

- **Framework**: migrated from the legacy FastMCP 1.0 bundled in the `mcp` SDK to standalone [FastMCP 3.x](https://gofastmcp.com), targeting the current MCP spec (2025-11-25, Streamable HTTP transport).
- **Layout**: flat `jobspy_mcp_server/` package replaced by a `src/jobspy_mcp/` layout; locking moved from `requirements.lock` to `uv.lock`.
- **Tool surface**: one focused `search_jobs` tool instead of four. Static catalogs became MCP resources (`jobspy://sites`, `jobspy://countries`) and search tips became the `job-search` prompt — this keeps the per-turn token cost of tool schemas low.
- **Output**: structured content with a declared output schema (`count`, `offset`, `results_per_site`, `sites_queried`, `truncated`, `next_offset`, `note`, `jobs[]`) alongside the JSON text block. Records are compact; descriptions are opt-in and truncated at 1,500 chars.
- **Robustness**: input validation with actionable errors (blank terms, empty site lists, unknown countries), a 60-job total response cap, a 120s scrape timeout, progress heartbeats every 5s, and proper MCP tool errors instead of transport crashes.
- **Ops**: proxy/CA-cert configuration via environment variables, optional bearer-token auth for HTTP mode, and Directory-ready tool annotations (`title`, `readOnlyHint`, `destructiveHint`).
- **Dependency**: `python-jobspy` is now installed from GitHub main (the PyPI release lags behind and is missing LinkedIn/Naukri fixes).

### Breaking changes

| 1.x | 2.0 |
|---|---|
| Tool `scrape_jobs_tool` | Renamed to `search_jobs`; results are JSON records, not formatted prose |
| Tools `get_supported_sites` / `get_supported_countries` | Removed — read resources `jobspy://sites` / `jobspy://countries` |
| Tool `get_job_search_tips` | Removed — use the `job-search` prompt |
| `python -m jobspy_mcp_server` | Use `python -m jobspy_mcp` (or the `jobspy-mcp-server` script) |
| `uv run mcp dev/run -m jobspy_mcp_server` | Removed — use `uv run jobspy-mcp-server` or the MCP Inspector |
| `pip install -r requirements.lock` | Removed — use `uv sync` |
| Docker image spoke stdio | Docker image now serves Streamable HTTP on port 8000 |
| `results_wanted` up to 100+, uncapped output | Capped at 50 per site and 60 jobs total per response |

### Migrating from 1.x

1. **Reinstall.** Pull the new code and run `uv sync` (delete any old `.venv` first if it predates the rewrite). `git` must be on your PATH for the GitHub-pinned `python-jobspy`.
2. **Update client configs.** The console script name is unchanged (`jobspy-mcp-server`), so configs that launch it via `uv run` keep working. Replace any config that used `python -m jobspy_mcp_server` or `mcp run` with the command shown in [Usage](#usage).
3. **Update tool calls / automations.** Call `search_jobs` instead of `scrape_jobs_tool`; parse the structured JSON output instead of formatted text; page with `offset`/`next_offset`. For board and country lists, read the resources instead of calling tools.
4. **Docker users.** The container is now an HTTP server: map port 8000 and connect to `http://host:8000/mcp` (set `JOBSPY_HTTP_TOKEN` for auth). For stdio, run the server directly on the host instead.

## Development

```bash
uv sync --extra dev
uv run pytest            # in-memory tests, scraping mocked
npx @modelcontextprotocol/inspector uv run jobspy-mcp-server   # interactive testing
bash scripts/build-mcpb.sh   # pack Claude Desktop bundle → dist/*.mcpb
```

## Docker (HTTP mode)

```bash
docker build -t jobspy-mcp-server .
docker run --rm -p 8000:8000 jobspy-mcp-server
```

The image serves Streamable HTTP on port 8000, with sessions on (same as `--http` without `--stateless`). To turn sessions off:

```bash
docker run --rm -p 8000:8000 jobspy-mcp-server \
  jobspy-mcp-server --http --stateless --host 0.0.0.0 --port 8000
```

Scraping from cloud IPs is likely to get blocked; pass proxies (`JOBSPY_PROXIES`) or prefer running locally.

## Distribution status

The supported distribution path is the **MCPB bundle** (Claude Desktop one-click install). Clone + `uv sync` is still the right workflow for development. Publishing to PyPI is currently blocked because `python-jobspy` is pinned to a git commit (PyPI rejects direct URL dependencies) — switch to a released version once upstream tags one. Hosted streamable-HTTP is possible if you bring your own proxies; job boards block datacenter IPs.

## Releasing

Every push to `main` runs tests, packs the MCPB, and updates the [`mcpb-latest`](https://github.com/chinpeerapat/jobspy-mcp-server/releases/tag/mcpb-latest) prerelease (stable filename `jobspy-mcp-server.mcpb`).

To cut a numbered GitHub Release:

1. Bump `version` in both `pyproject.toml` and `manifest.json` (they must match).
2. Commit, then tag and push:

```bash
git tag v0.2.1
git push origin v0.2.1
```

The tag must be `v` + the package version. CI attaches `jobspy-mcp-server-<version>.mcpb` to that release. Do not commit `dist/*.mcpb` — GitHub Releases is the publish location.

## License

MIT