# MCP server

> How do I connect an AI agent to BetterJobs over MCP, and what does each tool cost?

Source: https://docs.betterjobs.cc/agents/mcp-server/

BetterJobs runs a remote MCP server. An agent connected to it can search jobs, check whether a company is hiring and watch companies, at the same credit prices as the REST API.

| Setting   | Value                           |
| --------- | ------------------------------- |
| URL       | `https://mcp.betterjobs.cc/mcp` |
| Transport | Streamable HTTP                 |
| Auth      | Bearer API key                  |
| Plan      | Growth and above                |

Use a `bj_live_` key. See [Authentication](https://docs.betterjobs.cc/getting-started/authentication.md).

## Connect a client

**Claude Code**

Run once in your terminal. It adds the server to your Claude Code config.

```bash
claude mcp add --transport http betterjobs https://mcp.betterjobs.cc/mcp \
  --header "Authorization: Bearer $BETTERJOBS_API_KEY"
```

Check it with `claude mcp list`.

**Claude Desktop**

Add this to `claude_desktop_config.json` and restart Claude Desktop. The `mcp-remote` bridge connects the desktop app to a remote server.

claude\_desktop\_config.json

```json
{
  "mcpServers": {
    "betterjobs": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://mcp.betterjobs.cc/mcp",
        "--header",
        "Authorization:${BETTERJOBS_AUTH}"
      ],
      "env": {
        "BETTERJOBS_AUTH": "Bearer bj_live_..."
      }
    }
  }
}
```

**Cursor**

Add this to `.cursor/mcp.json` in your project, or `~/.cursor/mcp.json` for all projects. Cursor reads the key from your environment.

.cursor/mcp.json

```json
{
  "mcpServers": {
    "betterjobs": {
      "url": "https://mcp.betterjobs.cc/mcp",
      "headers": {
        "Authorization": "Bearer ${env:BETTERJOBS_API_KEY}"
      }
    }
  }
}
```

> Keep the key out of shared config
>
> A project-level config file is often committed. Reference an environment variable instead of pasting `bj_live_...` into it.

## Tools

Each tool maps to one REST endpoint and costs the same. Tool descriptions are written for the agent: when to call, what to pass, and what cheaper or lighter tool to try first.

| Tool              | Call it when                                                                                                                    | Inputs                                                                                      | Cost                                                                     | Ask the user first?                                  | Cheaper or lighter sibling                                                          | REST                                      |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | ---------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------- |
| `search_jobs`     | Find jobs matching filters. Call estimate\_search first if it may cost more than 50 credits, and ask the user.                  | Same body as POST /v1/jobs/search: filters (required), waterfall, limit (1 to 100), cursor. | 1 credit per unique job                                                  | Ask above 50 credits (estimate first)                | estimate\_search (free) to price it first                                           | `POST /v1/jobs/search`                    |
| `estimate_search` | Before any large search. Returns expected jobs, providers and credit range.                                                     | filters (required), waterfall, limit.                                                       | Free                                                                     | No                                                   | None. It is free.                                                                   | `POST /v1/jobs/search with dry_run: true` |
| `get_job`         | Fetch one job by id.                                                                                                            | id: a canonical job id (job\_...).                                                          | Free if already paid, else 1 credit                                      | No                                                   | None. Free for jobs you already paid for.                                           | `GET /v1/jobs/{id}`                       |
| `company_hiring`  | Full hiring profile for a company domain.                                                                                       | domain: company domain, no scheme or path.                                                  | 1 credit, every call                                                     | Ask before looping over many domains (1 credit each) | is\_hiring returns the same profile call. Call one, not both; each call is charged. | `GET /v1/companies/{domain}`              |
| `is_hiring`       | Yes / no / unknown answer for one company.                                                                                      | domain: company domain, no scheme or path.                                                  | 1 credit, every call                                                     | Ask before looping over many domains (1 credit each) | company\_hiring hits the same endpoint. Call one, not both; each call is charged.   | `GET /v1/companies/{domain}`              |
| `watch_company`   | Get told when a company opens jobs or starts or stops hiring. Needs webhooks (Pro and above); otherwise returns plan\_required. | domain, webhook\_url, events (optional; defaults to all job and company events).            | Free to create; 1 credit per job.opened delivered (free if already paid) | Always ask: ongoing charge with no cap               | is\_hiring for a one-off check                                                      | `POST /v1/watches`                        |
| `list_events`     | Read the event feed for watches and async searches.                                                                             | since (cursor from the last next\_cursor), limit (1 to 100).                                | Free                                                                     | No                                                   | None. It is free.                                                                   | `GET /v1/events`                          |
| `search_docs`     | Answer questions about the API from these docs.                                                                                 | query: a question in plain English.                                                         | Free                                                                     | No                                                   | None. It is free.                                                                   | `—`                                       |

`search_jobs` takes exactly the `POST /v1/jobs/search` body, so the [Filters](https://docs.betterjobs.cc/platform/filters.md) page applies as-is. The [Query Builder](https://docs.betterjobs.cc/getting-started/quickstart.md#4-build-your-own-query) prints a ready `search_jobs` call.

## The consent rule

Agents spend your credits. The server enforces your plan and your `max_credits`. The rest is a rule the agent should follow:

1. Before any search that may cost more than 50 credits, call `estimate_search`. It is free.
2. Show the user `credits_range` and `providers_planned` and ask before running it.
3. Run `search_jobs` with `waterfall.max_credits` set to what the user approved.
4. Keep the canonical job `id` values. `get_job` on a job you already paid for is free, so never search again just to re-read a job.
5. Never call `watch_company` without asking. Each `job.opened` it delivers costs 1 credit, and a watch has no credit cap.
6. `is_hiring` and `company_hiring` call the same endpoint and each call costs 1 credit. Call one, not both, and ask before looping over many domains.

Put the rule in your system prompt or project instructions:

```text
When using the BetterJobs MCP server: before any search_jobs call that may cost more
than 50 credits, call estimate_search, show me credits_range and providers_planned,
and wait for my approval. Always set waterfall.max_credits. Never call watch_company
without asking: each job.opened it delivers costs 1 credit, with no cap. Treat
is_hiring.value null as unknown, never as false.
```

> null is unknown
>
> `is_hiring` and `company_hiring` return `is_hiring.value: null` when there is not enough signal. An agent must say “unknown”, not “not hiring”. See [Provenance and confidence](https://docs.betterjobs.cc/concepts/provenance-and-confidence.md).

## Errors

Tools return the same error codes as the REST API, with the same fields. The ones agents hit most:

| Code                   | What the agent should do                                             |
| ---------------------- | -------------------------------------------------------------------- |
| `insufficient_credits` | Stop. Tell the user `credits_needed` and the `upgrade_url`.          |
| `plan_required`        | Tell the user the `required_plan`. Do not retry.                     |
| `unknown_filter`       | Fix the field named in `param`. The message suggests the right name. |
| `rate_limited`         | Wait `Retry-After` seconds, then retry once.                         |

Full list: [Errors](https://docs.betterjobs.cc/platform/errors.md).

## Related

- [Agent quickstart](https://docs.betterjobs.cc/agents/agent-quickstart.md)
- [llms.txt and Markdown](https://docs.betterjobs.cc/agents/llms-txt.md)
- [Credits and billing](https://docs.betterjobs.cc/concepts/credits-and-billing.md)
