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.
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"}'
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_jobsOne page of roles, newest first, with a total and a page count. Every argument is optional; with none, it returns the whole feed.
eng finds “Engineer”;
ngine finds nothing.
category is ignored, and the whole feed comes
back. Compare the filters object in the response with what you sent.
| Argument | Type | What 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.
| Argument | Type | What 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:
vendor and board_token is in this feed. Take the
pair from a search_jobs result or from get_facets.
search_jobs reports it the same way.
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:
| Field | What it holds |
|---|---|
total | Roles matching the filters, across all pages. |
page, pages, per_page | Where this page sits and how large it is. |
filters | The narrowings actually applied, echoed back: q, location, category, remote. |
failed_boards | Boards that did not answer. Always present; empty on a healthy feed. |
jobs | The 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
}
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.
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.