Job search MCP server and JSON API

JobQuery reads open roles live from 150 startup job boards and merges them into one feed. This page is for the developer connecting an AI agent or a script to that feed: the same roles a reader browses on the site are available over the Model Context Protocol and as JSON. No key, no account, no sign-up.

Connect over MCP

The server is at https://jobquery.io/mcp. It speaks the streamable HTTP transport: JSON-RPC over a single POST. A GET or DELETE answers 405, because this server never initiates a message and so has no stream to open. It announces itself as jobquery 1.1.0 and publishes tools only, with no prompts and no resources.

Add it to any client that supports remote MCP servers:

{
  "mcpServers": {
    "jobquery": { "type": "http", "url": "https://jobquery.io/mcp" }
  }
}

In Claude Code:

claude mcp add --transport http jobquery https://jobquery.io/mcp

Or call it directly, to see the tool definitions the server hands a client:

curl -s https://jobquery.io/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

The tools

All of them are read-only and run over the same feed the site renders. The server retrieves; it does not rank or interpret. There is no semantic search, no synonym expansion and no relevance order, so the calling model brings the semantics: several narrow keyword probes work far better than one natural-language sentence.

search_jobs

One page of roles, newest first, with a total and a page count. Every argument is optional; with none, it returns the whole feed.

ArgumentTypeWhat it does
query string, optional Keywords, ANDed. Omit for the whole feed.
location string, optional Place to narrow to, e.g. "New York" or "Germany".
category string, optional One of get_facets().categories. Anything else is ignored.
remote boolean, optional True to return only roles offered remotely.
page integer, optional 1-based page number. Out-of-range values give page 1.
per_page integer, optional Roles per page, default 25, capped at 100.

get_job

One posting, addressed by the (vendor, board_token, id) triple that every search_jobs result carries. All three are needed because an id is unique only within its board. The posting is fetched live from the employer's board, not from the cached feed, so a role that has been filled or withdrawn says so. It returns the structured fields plus description_excerpt, and there is no argument for more of the body.

ArgumentTypeWhat it does
vendor string, required ATS vendor, e.g. "greenhouse".
board_token string, required The company's board token on that vendor.
id string, required The posting id, as returned by search_jobs.

get_facets

The vocabularies search_jobs matches on. Call it first. It returns categories, a closed list of 14 values; companies, the boards in the feed; locations, the most common place labels, as examples of spelling and not a menu; and total_postings beside uncategorized_postings, because a role whose department maps onto no category is reachable only in the unfiltered feed.

It takes no arguments.

Every result carries its payload twice: as structuredContent for a client that reads typed results, and as the same JSON in a text block for one that does not. A failure the model should act on comes back with isError: true and a sentence saying what happened. The cases are kept apart because they call for different responses:

The JSON routes

The same feed without MCP. Both routes are shaped by the code that shapes the tool results, so a route and a tool cannot answer differently about the same role.

GET /jobs/list.json

What search_jobs returns. Query parameters: q, location, category, remote (1 for roles offered remotely), page and per_page, which defaults to 25 and is capped at 100.

curl -s 'https://jobquery.io/jobs/list.json?q=staff+rust&remote=1&per_page=5'

The response is one page of roles inside an envelope:

FieldWhat it holds
totalRoles matching the filters, across all pages.
page, pages, per_pageWhere this page sits and how large it is.
filtersThe narrowings actually applied, echoed back: q, location, category, remote.
failed_boardsBoards that did not answer. Always present; empty on a healthy feed.
jobsThe roles. Each has its title, company, locations, categories, the employer's url, and the vendor, board_token and id that address it.

GET /jobs/{vendor}/{board_token}/{id}.json

What get_job returns: one posting, fetched live from its board. A role that has been filled answers 404. An invented example, to show the shape:

{
  "title": "Staff Software Engineer",
  "org_name": "Example Co",
  "posted_at": "2026-01-15T09:00:00Z",
  "department": "Engineering",
  "category": "engineering",
  "locations": [
    "Remote - US"
  ],
  "employment_type": "full_time",
  "workplace_type": "remote",
  "remote": true,
  "salary_min": 180000,
  "salary_max": 220000,
  "salary_currency": "USD",
  "salary_period": "year",
  "salary_raw": "$180,000 - $220,000",
  "vendor": "greenhouse",
  "board_token": "exampleco",
  "id": "1234567",
  "url": "https://job-boards.greenhouse.io/exampleco/jobs/1234567?utm_source=jobquery.io&utm_medium=referral",
  "path": "/jobs/greenhouse/exampleco/1234567",
  "description_excerpt": "(the first 400 characters of the posting, as plain text)",
  "description_excerpt_limit": 400
}

Excerpts only, on every route

Nothing here returns a full job description. A detail response carries description_excerpt, the first 400 characters of the posting as plain text, and the feed carries no description at all. The employer wrote that prose and holds the copyright in it, so the excerpt is for triage and every result carries the canonical url of the posting on the employer's own board. Send people there to read it in full and to apply. The limit is the same over MCP, over JSON and on the site.

Read failed_boards

The feed is assembled from whichever boards answered. When some did not, the response names them in failed_boards, with each board's name, vendor, board_token, status and error. A result built from fewer boards than the whole roster is incomplete, so an empty result then means “not found in what answered”, not “no such role exists”. Do not report absence as fact while the list is non-empty.

The same reference as plain text for a model to read: llms.txt. Browse the feed by role, place or remote or by company.