# MCP server

A remote MCP server at api.jobopportunitiesapi.org/mcp — Streamable HTTP — so an AI agent in Claude, Cursor, Windsurf, VS Code or Cline can search live job postings with the same key, metering and plan limits as the REST API.

**What this covers:** What this is; The six tools; Client configuration; Raw JSON-RPC, if your client speaks it directly; Auth, metering and limits.

**Assumed knowledge:** Nothing about JOA. Some familiarity with the Model Context Protocol helps but is not required.

**Canonical HTML:** https://jobopportunitiesapi.org/docs/mcp  
**Machine-readable index:** https://jobopportunitiesapi.org/docs/ai/index.md  
**Last verified:** 2026-10-01  
**Superseded by:** the live API at https://api.jobopportunitiesapi.org and its spec at https://jobopportunitiesapi.org/openapi.json — where this file and the API disagree, the API is right.

---

> **One block of JSON, and a model can query the ledger** — There is no SDK to install and no server to run yourself — this is a remote server you point a client at. Paste the config for your client below, or hand a model the raw JSON-RPC calls further down the page.

---

<a id="mcp-what-it-is"></a>

## 1. What this is

A single remote endpoint speaking the Model Context Protocol over Streamable HTTP — the same auth, metering and data as the REST API, reached a second way.

### 1.1 In depth

`POST https://api.jobopportunitiesapi.org/mcp` carries every JSON-RPC 2.0 message: `initialize`, `notifications/initialized`, `ping`, `tools/list`, `tools/call`. Every response is a single `application/json` object — never Server-Sent Events, because nothing here pushes unprompted. `GET` on the same path answers `405`, which is spec-legal for a server with nothing to stream. No session id is issued: the server is stateless, and every call carries its own `Authorization` header.

Six tools wrap six parts of the REST API, and each tool call replays through the exact same request path a direct `curl` of the wrapped endpoint would use — the same handler, the same auth check, the same metering. There is no second implementation of any of this to drift out of sync with the API you already know.

### 1.2 Exact contract

- **Endpoint** — `https://api.jobopportunitiesapi.org/mcp` — the API host, not the website host.
- **Transport** — Streamable HTTP, a single endpoint for both verbs, per the MCP specification.
- **Protocol version** — `2025-06-18` — the classic `initialize` handshake every current MCP client (Claude, Cursor, Windsurf, VS Code, Cline) actually implements.
- **Session state** — None. Stateless by construction — no `Mcp-Session-Id`, no server-held conversation.
- **Registry name** — `org.jobopportunitiesapi/mcp` — reverse-DNS of `jobopportunitiesapi.org`, for the MCP registry.

**See also**

- [The six tools](./mcp.md#mcp-tools)
- [Client configuration](./mcp.md#mcp-clients)
- [Auth, metering and limits](./mcp.md#mcp-auth-limits)

<a id="mcp-tools"></a>

## 2. The six tools

Two need no key at all — market_signals and coverage. The other four need a free API key, same as the REST routes they wrap.

### 2.1 In depth

| Tool | Wraps | Key? |
| --- | --- | --- |
| `search_jobs` | `GET /v1/jobs` | Needed. |
| `get_job` | `GET /v1/jobs/{id}` | Needed. |
| `company_hiring` | `GET /v1/companies/{slug}` plus the keyless open-roles-history trend | Hybrid — the 30/90/365-day trend is keyless; the full roster needs one. |
| `market_signals` | `GET /public/market-signals` (and its `/history`) | Never. Keyless statistics, like the REST route. |
| `coverage` | `GET /public/coverage` plus `/public/stats` | Never. |
| `changes_since` | `GET /v1/changes` | Needed, and only on the Growth plan or above — same gate as the REST route. |

A row-serving tool called with no `Authorization` header never reaches the database: it answers a structured refusal naming [the free-key page](https://jobopportunitiesapi.org/login?ref=mcp), the same shape every tool's own description tells the calling model to expect — never a bare 401 the model has to interpret.

### 2.2 Exact contract

- **`search_jobs`** — Filters: country, city, US state, category/exclude_category, seniority, remote, remote_confirmed, employment_type, source_type, provider, company/company_domain, title, q, description_contains, has_salary, min_salary/max_salary, posted_after, verified_after, status, quality, include_poster_type, require_fields, include_description, limit (1-200, default 25), cursor.
- **`get_job`** — id (required — uuid or slug), include_closed, quality, include_poster_type.
- **`company_hiring`** — slug (required), history_days (30, 90 or 365), include_open_jobs, jobs_limit (1-50, default 10).
- **`market_signals`** — country, role_family, seniority, include_history, history_days (30, 90 or 365) — every filter optional; omit all for the whole-dataset aggregate.
- **`coverage`** — include_countries, include_employers — both optional booleans.
- **`changes_since`** — since (required — RFC3339 timestamp or a previous next_since cursor), limit (1-5000, default 500), event_type (created/updated/withdrawn/delisted, filtered client-side after the call so it does not change what is metered).

Every tool's `inputSchema` is full JSON Schema, served live by `tools/list` itself — the table above is for reading, not for hand-typing into a client; a conformant client reads the schema from the server.

> **Job and company description text is third-party and untrusted** — Every tool's own description tells the calling model this explicitly, and the server strips HTML-like pseudo-tags from scraped text, truncates long descriptions and labels them as untrusted before they reach the model — the same prompt-injection hygiene the REST API's description fields need, because the text came from an employer's own site, not from JOA.

**See also**

- [Auth, metering and limits](./mcp.md#mcp-auth-limits)
- [GET /v1/jobs](./endpoints-jobs.md#endpoint-jobs)
- [The job row](./api-fields.md#job-fields)

<a id="mcp-clients"></a>

## 3. Client configuration

The same JSON block works in Claude Desktop, Claude.ai, Cursor, Windsurf, VS Code and Cline — only the file it goes in changes.

### 3.1 In depth

Claude Desktop and Claude.ai use the `streamableHttp` shape below, either pasted into **Settings → Connectors → Add custom connector**, or written directly into `claude_desktop_config.json`.

Claude Desktop / Claude.ai

```json
{
  "mcpServers": {
    "joa": {
      "type": "streamableHttp",
      "url": "https://api.jobopportunitiesapi.org/mcp",
      "headers": { "Authorization": "Bearer YOUR_API_KEY" }
    }
  }
}
```

Cursor, VS Code and Cline all read the plain shape below — Cursor's project or global `.cursor/mcp.json`, VS Code's `.vscode/mcp.json` (or Command Palette → "MCP: Add Server" → HTTP), and Cline's **MCP Servers → Configure MCP Servers**, which reads the identical `mcpServers` block.

Cursor / VS Code / Cline

```json
{
  "mcpServers": {
    "joa": {
      "url": "https://api.jobopportunitiesapi.org/mcp",
      "headers": { "Authorization": "Bearer YOUR_API_KEY" }
    }
  }
}
```

Windsurf: **Cascade → Plugins → MCP Servers → add a custom server** with the same URL and header, or edit `~/.codeium/windsurf/mcp_config.json` directly — same JSON shape as Cursor's above.

### 3.2 Exact contract

ChatGPT: OpenAI's unified plugin directory needs a ZIP submission with an identity-verified developer account and is not yet submitted. Until then, any MCP-capable ChatGPT developer-mode client that accepts a raw Streamable HTTP URL with a static header can use the Cursor/VS Code configuration above unchanged.

> **Exactly one door** — `Authorization: Bearer YOUR_API_KEY` as an HTTP header on the MCP connection. A key in the URL or passed as a tool argument is never accepted — the same rule as the REST API.

**See also**

- [Raw JSON-RPC, if your client speaks it directly](./mcp.md#mcp-raw-jsonrpc)
- [Three accepted spellings, and why](./api-authentication.md#auth-tolerant)
- [Step two — get a key](./quickstart.md#qs-get-key)

<a id="mcp-raw-jsonrpc"></a>

## 4. Raw JSON-RPC, if your client speaks it directly

A handshake and a tool call, as curl — useful for testing a connection or building a client that is not on the list above.

### 4.1 In depth

`initialize` first, then a tool call. The handshake needs no key; a keyed tool call does.

### 4.2 Exact contract

Handshake

```bash
curl -s https://api.jobopportunitiesapi.org/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
       "params":{"protocolVersion":"2025-06-18","capabilities":{},
                 "clientInfo":{"name":"curl","version":"1.0"}}}'
```

A tool call

```bash
curl -s https://api.jobopportunitiesapi.org/mcp \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
       "params":{"name":"search_jobs","arguments":{"country":["DE"],"category":["Engineering"]}}}'
```

A keyless tool call (`market_signals`, `coverage`) is identical but without the `Authorization` header.

**See also**

- [What this is](./mcp.md#mcp-what-it-is)
- [The six tools](./mcp.md#mcp-tools)

<a id="mcp-auth-limits"></a>

## 5. Auth, metering and limits

Every tool call is metered exactly like the REST endpoint it wraps — same plan, same monthly allowance, same rate limit. There is no separate MCP quota.

### 5.1 In depth

A tool call replays through the same request path the matching REST endpoint uses, so a `search_jobs` call and a direct `curl` of `/v1/jobs` with the identical filter and the same key charge the identical number of records — there is one metering implementation, reached two ways, not two that could drift apart.

`changes_since` on a plan without the delta-feed entitlement gets the same `403` a direct `curl` of `/v1/changes` would get. A free Explore key (1,000 records a month, no card) is enough to try every keyed tool — [get one here](https://jobopportunitiesapi.org/login?ref=mcp).

### 5.2 Exact contract

| Thing | Behaviour |
| --- | --- |
| No `Authorization` header, keyed tool | `isError: true`, text naming `/login?ref=mcp` — no internal call made, no record charged. |
| `changes_since`, no `delta_feed` entitlement | Same `403` as `GET /v1/changes` on that plan. |
| Record allowance | Shared with the REST API — one meter per key, see [the record meter](./account-record-meter.md). |
| Rate limits | Same per-plan request ceilings as the REST API — see [rate limits](./api-rate-limits.md). |

**See also**

- [What counts as a record](./account-record-meter.md#meter-what-counts)
- [402 — the licence, not the throttle](./api-errors.md#error-402)
- [The two entitlements](./account-plans.md#plan-entitlements)

---

## Where to go next

This file is part of **Start here**. Others in the same group:

- [Overview](./overview.md) — What the Job Opportunities API is, what it deliberately is not, and the ten minutes of reading that will save you the most time.
- [Quickstart](./quickstart.md) — From nothing to a real response in one command, and to an authenticated one in about a minute. No card at any point.
- [If you are a program](./for-agents.md) — This documentation has a plain-Markdown mirror with no JavaScript, no redirects and no browser required. Here is where it is and what is in it.
- [How to read this site](./conventions.md) — Three tiers on every concept, a copyable link on every section, and a rule about numbers that explains why almost nothing here is typed by hand.
- [Beta & coming soon](./beta.md) — What is live today, labelled BETA and openly partial, and what is still being built. Nothing on this page is a promise of a ship date.

Always useful:

- [index.md](./index.md) — the map of every file here
- [BUILD-A-SITE.md](./BUILD-A-SITE.md) — the paste-whole brief for building against this API
- [quickstart.md](./quickstart.md) — zero to a first authenticated response
- [api-errors.md](./api-errors.md) — every status code and whether to retry it
