MCP · worked examples

What a job postings MCP server can do: worked examples

The MCP server page lists every tool and its input schema. This page shows what agents actually do with them: the prompt you give, the tool-call sequence the agent runs, and the JSON that comes back at each step. Responses are abridged to the fields each step uses — full shapes are in the API reference.

Connect

Paste this into your client's server config, replacing the placeholder with your own key. Endpoint https://mcp.reqbeat.com/mcp, authenticated with the X-API-Key request header.

{
  "mcpServers": {
    "reqbeat": {
      "url": "https://mcp.reqbeat.com/mcp",
      "headers": { "X-API-Key": "YOUR_REQBEAT_API_KEY" }
    }
  }
}

No key yet? Get an API key. Free, no card.

Client setup, per connector

The server speaks streamable HTTP and authenticates with the X-API-Key header, so it works in any MCP-capable client. The four we see most:

Claude Code

One command, then every tool is available in the session:

claude mcp add --transport http reqbeat \
  https://mcp.reqbeat.com/mcp \
  --header "X-API-Key: $REQBEAT_API_KEY"

Claude Desktop

Claude Desktop's config launches local servers, so bridge the remote endpoint with mcp-remote. Add this to claude_desktop_config.json (Settings → Developer → Edit Config) and restart the app:

{
  "mcpServers": {
    "reqbeat": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote",
        "https://mcp.reqbeat.com/mcp",
        "--header", "X-API-Key: YOUR_REQBEAT_API_KEY"
      ]
    }
  }
}

Cursor

Cursor reads remote servers with custom headers directly. Put this in .cursor/mcp.json in your project (or ~/.cursor/mcp.json globally):

{
  "mcpServers": {
    "reqbeat": {
      "url": "https://mcp.reqbeat.com/mcp",
      "headers": { "X-API-Key": "YOUR_REQBEAT_API_KEY" }
    }
  }
}

A custom agent over the Anthropic SDK

For your own agent, connect with the official MCP SDK (the connection is client-side, so the X-API-Key header works as-is) and hand the discovered tools to the Anthropic SDK's tool runner. Python shown; the TypeScript SDK has the same helpers:

# pip install anthropic[mcp]
import asyncio, os
from anthropic import AsyncAnthropic
from anthropic.lib.tools.mcp import async_mcp_tool
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

client = AsyncAnthropic()
REQBEAT = "https://mcp.reqbeat.com/mcp"
HEADERS = {"X-API-Key": os.environ["REQBEAT_API_KEY"]}

async def main():
    async with streamablehttp_client(REQBEAT, headers=HEADERS) as (read, write, _):
        async with ClientSession(read, write) as mcp:
            await mcp.initialize()
            tools = (await mcp.list_tools()).tools
            runner = client.beta.messages.tool_runner(
                model="claude-opus-5",
                max_tokens=16000,
                tools=[async_mcp_tool(t, mcp) for t in tools],
                messages=[{"role": "user", "content":
                    "Is braze.com hiring? If so, rank it by hiring pulse "
                    "and watch it for new postings."}],
            )
            async for message in runner:
                print(message)

asyncio.run(main())

Example 1 — the core loop: from a domain to a webhook that fires on new postings

The sequence almost every workflow builds on. The agent resolves a company, gates it on whether it is hiring, reads its pulse, then saves a watch so new postings push to your endpoint — no polling afterwards.

The prompt:

"Is Braze hiring? If they are and hiring is picking up, watch them and push new postings to https://hooks.example.com/reqbeat."

Step 1 — resolve the company. find_company turns a domain or name into the company_id every other tool takes. Never billed.

→ find_company({"domain": "braze.com"})

{
  "companies": [
    {
      "company_id": 5512,
      "company_name": "Braze",
      "company_domain": "braze.com",
      "country_code": "US",
      "coverage_status": "ats_direct_hit"
    }
  ]
}

Step 2 — gate cheaply. is_hiring is the yes/no check before spending a richer call. A company with coverage_status: "no_ats_signal" is unknown, not quiet — don't score it as a negative.

→ is_hiring({"company_id": 5512})

{
  "is_hiring": true,
  "open_req_count": 48,
  "coverage_status": "ats_direct_hit"
}

Step 3 — read the pulse. hiring_pulse answers "is hiring picking up": reqs opened in the last 30 days, velocity against the 30 before, direction, surge flag. A company still being crawled returns {"job_id": …, "status": "crawling"} — poll again later rather than reading it as "not hiring".

→ hiring_pulse({"company_id": 5512})

{
  "new_roles_30d": 23,
  "velocity": 1.4,
  "direction": "up",
  "is_surge": false,
  "momentum": 5.1
}

Step 4 — register the delivery target. register_webhook is idempotent: the same URL twice returns the same id, so retries never leave two endpoints. The signing secret is returned once, on this call only.

→ register_webhook({"url": "https://hooks.example.com/reqbeat"})

{
  "id": 42,
  "url": "https://hooks.example.com/reqbeat",
  "secret": "0f52c1…9be4"
}

Step 5 — save the watch. watch_company subscribes the company's hiring events to that endpoint. opened is the event to treat as a fresh trigger; reposted is a req that came back, not a new opening.

→ watch_company({
    "company_id": 5512,
    "event_types": ["opened"],
    "webhook_endpoint_id": 42
  })

{
  "id": 311,
  "company_id": 5512,
  "event_types": ["opened"],
  "webhook_endpoint_id": 42
}

What lands on your endpoint when Braze posts a new req — the same signed envelope documented on the webhook reference, abridged here. Verify X-Reqbeat-Signature before acting on it:

{
  "envelope": "reqbeat.webhook.v1",
  "watch_id": 311,
  "webhook_endpoint_id": 42,
  "events": [
    {
      "event_type": "opened",
      "company_id": 5512,
      "company_name": "Braze",
      "req_key": "greenhouse:braze:7210045",
      "title": "Senior Platform Software Engineer I",
      "url": "https://boards.greenhouse.io/braze/jobs/7210045",
      "observed_at": "2026-11-09T07:12:40+00:00"
    }
  ]
}

Prefer pull over push? get_changes is the same ledger as a replayable feed: events with event_seq greater than your cursor, ascending, plus a next_cursor to resume from.

Example 2 — GTM engineer: qualify an account list

The prompt:

"Here are 40 target domains. Which should we work this week, and why?"

The agent runs the gate-then-rank loop per domain — the two cheap calls first, the richer one only for accounts that clear the gate:

for each domain:
  → find_company({"domain": domain})        // never billed
  → is_hiring({"company_id": id})           // never billed; gate
  → hiring_pulse({"company_id": id})        // rank on velocity,
                                            // direction, is_surge

then, for the account you're about to work:
  → pre_action_brief({"company_id": id})
  // one bounded round-trip: pulse + top open reqs (deduped
  // across boards) + first-hire-by-function events + reposted,
  // hard-to-fill reqs + ATS-vendor migrations

pre_action_brief exists so the "write me an opener for this account" step doesn't take five calls. Need the full req list instead of the brief's top slice? get_open_reqs returns every active req, deduped across boards, filterable by function and country. The complete recipe — system prompt included — is at the hiring-pulse agent recipe.

Example 3 — automation builder: wire hiring signals into outreach

The prompt:

"Find live senior data-engineer roles at product companies in Germany and the US. For each, draft an intro that references the actual posting."

find_qualified_reqs collapses the whole ICP into one call — countries, title keywords and seniority together, agencies and expired postings excluded by default:

→ find_qualified_reqs({
    "criteria": {
      "company": {"country": ["United States", "Germany"]},
      "req": {"q": ["data engineer"], "seniority_level": ["senior"]}
    }
  })

{
  "rows": [
    {
      "company_id": 5512,
      "company_name": "Braze",
      "req_key": "greenhouse:braze:7210112",
      "title": "Senior Data Engineer",
      "url": "https://boards.greenhouse.io/braze/jobs/7210112",
      "seniority_level": "senior",
      "skills": ["python", "airflow", "snowflake"]
    }
  ],
  "next_cursor": 184207
}

Each row names the company and carries the posting URL — enough to personalize without another lookup. For free-text search across titles there's search_jobs, and get_role pulls one role's detail by the company_id + req_key pair a search row already carries. When an account converts, the agent reports it back — so your outcome data accrues against the signals that produced it, visible only to your account:

→ write_outcome({
    "company_id": 5512,
    "outcome": "meeting_booked",
    "observed_at": "2026-11-09T09:00:00Z",
    "req_key": "greenhouse:braze:7210112"
  })

{ "id": 87 }

Example 4 — agency: monitor clients' competitors

The prompt:

"Watch these five competitors of our client. Tell me when any of them starts or stops hiring for growth roles."

Same core loop, multiplied — resolve each name, save a watch for opened and closed events. By name, find_company returns up to five candidates with a match_confidence, so the agent can pick the exact-name match. The part that matters for a retainer: list_watches reports each watch's heartbeat, so a watch that is silent because nobody is hiring reads differently from one whose endpoint is failing:

→ list_watches({})

{
  "watches": [
    {
      "id": 311,
      "company_id": 5512,
      "event_types": ["opened", "closed"],
      "webhook_endpoint_id": 42,
      "status": "firing",
      "last_fired_at": "2026-11-08T16:02:11Z",
      "fires_last_7d": 9,
      "fires_last_30d": 31,
      "rate_limited": false
    }
  ]
}

When the engagement ends, cancel_watch frees the slot — it's idempotent, so cancelling twice succeeds twice. A free key holds a limited number of watches at once; cancelling returns the slot.

Cost and correctness notes

  • find_company, is_hiring, list_watches, cancel_watch and write_outcome are never billed. register_webhook is free; the watch that follows is what meters.
  • who_is_hiring_for and find_qualified_reqs bill per company or row returned ($0.10 each) — an empty result bills nothing.
  • Errors are explicit, never quietly partial: an unresolvable geo, an unknown function name, or an invalid event_type each return an error that names the rule. Let the agent read them — the messages are written for it.
  • Pages are bounded. An oversized limit is rejected rather than truncated — page through with the cursor instead.
  • The corpus is LinkedIn-excluded (ATS tenants plus public job boards and aggregators) and freshness-floored; the method page states the sample and the known biases.

Every example above runs on the free tier. The key is on screen at signup.