JobsPipe

Search live job postings from 30+ job boards, ATS feeds and public employment services, normalized into one schema; read a posting in full; save a search as a signal to be notified of new matches; detect a company's tech stack. Remote server with OAuth sign-in, free account to start.

Documentation

MCP server

Connect AI agents and MCP clients to JobsPipe — search live job postings over the Model Context Protocol, signing in with OAuth or an API key.

JobsPipe runs a Model Context Protocol server so AI agents and MCP-capable clients (Claude, ChatGPT, Cursor, and custom MCP hosts) can search live job postings directly, without you writing any HTTP code.

The server speaks MCP over Streamable HTTP at:

https://mcp.jobspipe.dev/mcp

How it works

The MCP server is a thin layer over the JobsPipe API. Every tool call that needs data is forwarded to the API under your account, so:

  • Results, sources, and freshness are identical to the REST API.
  • Usage counts against your plan, and your plan's quota and rate limits apply.

There are two ways to authenticate, and both land on the same account.

OAuth sign-in (default)

Clients that support MCP authorization — Claude and ChatGPT connectors, Claude Code, and other hosts that implement OAuth discovery — only need the URL. When a client connects without credentials, the server answers 401 with a WWW-Authenticate header pointing at its protected-resource metadata:

WWW-Authenticate: Bearer realm="jobspipe", resource_metadata="https://mcp.jobspipe.dev/.well-known/oauth-protected-resource"

The client follows that link to the authorization server at https://api.jobspipe.dev (metadata at /.well-known/oauth-authorization-server), registers itself, and sends you through a JobsPipe sign-in and consent screen. It uses the authorization-code flow with PKCE (S256) and refresh tokens, and then sends the access token as Authorization: Bearer <token> on every request. No key needs to be copied anywhere.

The consent screen names the app, shows the redirect URLs it registered, and lists what it is asking for. Approving lets that app search jobs and scan tech stacks as you — every call counts against your plan, the same as your own — and create or change your saved signals. It cannot see your password, change your billing, or reach anything outside your own account. Approval belongs to the account you are signed in to at that moment, and the app is only ever sent back to a redirect URL it registered.

If a token expires or is revoked, the server answers 401 with Access token is invalid or expired. Reconnect to continue. — reconnect the client to sign in again.

API key header

For clients without OAuth support, scripts, and CI, use the same API key you use for the REST API — a key that starts with jp_live_. Create or copy one from your dashboard and send it in either header:

Authorization: Bearer jp_live_xxxxxxxxxxxxxxxxxxxxxxxx
x-api-key: jp_live_xxxxxxxxxxxxxxxxxxxxxxxx

x-api-key is read only when there is no Authorization: Bearer header. A bearer value that does not start with jp_live_ is treated as an OAuth access token.

A connection with no credentials at all is rejected with 401 Unauthorized. A jp_live_ key is not validated when you connect, only when a tool reaches the API — see Limits and errors.

Install it

Pick your client. Every route lands on the same server and the same account.

Claude Code

claude mcp add --transport http jobspipe https://mcp.jobspipe.dev/mcp --scope user

Then run /mcp inside Claude Code and choose Authenticate for jobspipe — a browser opens, you sign in to JobsPipe, and the tools appear. Drop --scope user to add it to the current project only, or use --scope project to write a .mcp.json your team can commit.

To use an API key instead of signing in, pass it as a header (and skip the /mcp authenticate step):

claude mcp add --transport http jobspipe https://mcp.jobspipe.dev/mcp \
  --scope user --header "Authorization: Bearer jp_live_xxxxxxxxxxxxxxxxxxxxxxxx"

A committed .mcp.json reads the key from the environment, so no secret is checked in:

{
  "mcpServers": {
    "jobspipe": {
      "type": "http",
      "url": "https://mcp.jobspipe.dev/mcp",
      "headers": {
        "Authorization": "Bearer ${JOBSPIPE_API_KEY}"
      }
    }
  }
}

Claude (web, desktop and mobile)

  1. Open Settings → Connectors and click Add custom connector.
  2. Paste https://mcp.jobspipe.dev/mcp as the remote MCP server URL.
  3. Click Connect. Claude sends you to a JobsPipe sign-in and consent screen, and that is the whole setup — there is no key to paste and no client ID to fill in under Advanced settings.

On Team and Enterprise plans an owner adds the connector once under Organization settings → Connectors, and each member then connects their own JobsPipe account from Settings → Connectors.

ChatGPT

ChatGPT connects to JobsPipe as a plugin backed by the MCP server. It cannot send an API key, so sign-in is the only route, and it needs Developer mode once.

  1. Open Settings → Security and login and turn on Developer mode. It has to stay on while the plugin is installed.
  2. Open Settings → Plugins and click the + to create a plugin. Name it JobsPipe and enter https://mcp.jobspipe.dev/mcp as the MCP server URL. Leave Authentication on OAuth.
  3. Click Create, then Connect. ChatGPT registers itself, opens a JobsPipe sign-in and consent screen, and the tools become available in a chat.

Two things that look like the plugin but are not: a JobsPipe entry installed from Browse plugins that shows curl commands and asks for an API key is a skill document, not this server, and ChatGPT will answer that it has "no usable API key"; remove it and create the plugin above. And if ChatGPT never shows a sign-in, Developer mode is off.

ChatGPT cannot send an API key to a connector, so OAuth is the only route there. The server also answers the search and fetch tools that ChatGPT's deep research and company-knowledge features require, so JobsPipe can be used as a research source and every job it cites links back to the original posting.

Cursor

Or add it by hand — ~/.cursor/mcp.json for every project, .cursor/mcp.json inside one project:

{
  "mcpServers": {
    "jobspipe": {
      "url": "https://mcp.jobspipe.dev/mcp"
    }
  }
}
{
  "mcpServers": {
    "jobspipe": {
      "url": "https://mcp.jobspipe.dev/mcp",
      "headers": {
        "Authorization": "Bearer ${env:JOBSPIPE_API_KEY}"
      }
    }
  }
}

Cursor has no type field for remote servers — a url is enough. Open Settings → MCP to confirm jobspipe is connected, and to sign in if you left headers out.

VS Code and GitHub Copilot

Or add it to .vscode/mcp.json — note the wrapper key is servers, not mcpServers:

{
  "servers": {
    "jobspipe": {
      "type": "http",
      "url": "https://mcp.jobspipe.dev/mcp"
    }
  }
}

To send an API key instead of signing in, let VS Code prompt for it once and keep it out of the file:

{
  "servers": {
    "jobspipe": {
      "type": "http",
      "url": "https://mcp.jobspipe.dev/mcp",
      "headers": { "Authorization": "Bearer ${input:jobspipe-api-key}" }
    }
  },
  "inputs": [
    {
      "type": "promptString",
      "id": "jobspipe-api-key",
      "description": "JobsPipe API key",
      "password": true
    }
  ]
}

Windsurf

In ~/.codeium/windsurf/mcp_config.json, remote servers use serverUrl:

{
  "mcpServers": {
    "jobspipe": {
      "serverUrl": "https://mcp.jobspipe.dev/mcp",
      "headers": {
        "Authorization": "Bearer ${env:JOBSPIPE_API_KEY}"
      }
    }
  }
}

Any other MCP client

Point it at https://mcp.jobspipe.dev/mcp over Streamable HTTP. Clients that implement MCP authorization need nothing else; the rest send the key as a header.

{
  "mcpServers": {
    "jobspipe": {
      "type": "http",
      "url": "https://mcp.jobspipe.dev/mcp",
      "headers": {
        "Authorization": "Bearer jp_live_xxxxxxxxxxxxxxxxxxxxxxxx"
      }
    }
  }
}

Once connected, the tools below appear automatically and the agent can call them.

Tools

ToolDescription
searchFind postings and get back results to cite, each with an id, a title and a link.
fetchRead one posting in full by the id a search result carried.
search_jobsSearch live job postings from 30+ sources, normalized into one schema, with every filter.
create_signalSave a search and be told when something new matches it, by email, Slack or a signed webhook.
list_signalsList the signals on your account, with their filters, destinations and when each was last checked.
detect_company_tech_stackDetect the technologies a company serves on its domain, with confidence scores.
search_documentationSearch these docs and get back the matching sections with their text and a link.
list_pricing_plansList JobsPipe plans with monthly price, job quota, and max results per call.
get_account_infoShow which account the connection is signed in to, its plan, and the credits used and left this month.

search, fetch, search_jobs and detect_company_tech_stack call the API and count against your plan. A job is charged once per calendar month, so a fetch of a posting that search already returned is free. search_documentation and list_pricing_plans are answered by the MCP server itself and use no credits. get_account_info, create_signal and list_signals read or write your account and use no credits either; a signal costs no job credits to evaluate.

search and fetch

search and fetch are the pair that research and connector features look for by name — ChatGPT's deep research and company knowledge among them — so a JobsPipe search can be cited in a report like any other source. They are a simpler view of the same corpus search_jobs serves.

search takes a query and, optionally, country_code (ISO 3166-1 alpha-2), city, remote, posted_within_days and limit (1 to 50, default 10). The query matches posting titles: it is tried as written first, and if nothing matches it is tried again as the significant words in it, so a whole question still returns something. It answers a results array of { id, title, url }, where url is the posting itself and is what gets cited, and a usage block saying what the call cost.

{
  "results": [
    {
      "id": "b1f3c0d2e4a5",
      "title": "Senior Data Engineer - Acme Corp (Berlin, Germany)",
      "url": "https://example.com/jobs/b1f3c0d2e4a5"
    }
  ],
  "usage": { "credits_charged": 1, "jobs_already_paid": 0 }
}

usage says what that search cost: credits_charged is the credits it used, and jobs_already_paid is how many of the postings were free because your account already paid for them this calendar month. It is absent on an account that is not billed per job.

fetch takes the id of a result and answers that one posting as { id, title, text, url, metadata }. text is the posting as readable prose — role, employer, location, work arrangement, employment type, seniority, salary, dates, skills and the posting body — and metadata carries the same facts as individual string fields, plus credits_charged and jobs_already_paid for what reading it cost. An id that no longer resolves comes back as a tool error naming it; ids stop resolving once a posting closes.

For anything these two do not cover — salary floors, skills, sources, visa stance, language, industry or occupation codes, paging past 50 — use search_jobs below.

search_jobs

Every filter is optional and combined with AND. Array filters ending in _or match any of their values.

Text and company

ParameterTypeDescription
job_title_orstring[]Match jobs whose title contains any of these phrases.
job_title_notstring[]Exclude jobs whose title contains any of these phrases.
description_orstring[]Match jobs whose description contains any of these phrases.
company_name_orstring[]Match jobs from any of these exact company names.
company_name_partial_match_orstring[]Match jobs whose company name contains any of these, e.g. ["acme"] finds "Acme Corp" and "Acme Ltd".
employer_type_orstring[]Keep only these kinds of employer: employer (the company itself), agency, broker.
employer_type_notstring[]Drop these kinds of employer. ["agency","broker"] keeps only jobs posted by the hiring company.
min_employee_countnumberCompanies with at least this many employees. Jobs whose company size is unknown are dropped unless include_unknown_size is true.
max_employee_countnumberCompanies with at most this many employees. Same rule for unknown sizes.
include_unknown_sizebooleanKeep jobs whose company size is unknown when filtering by employee count. Most postings carry no size, so a size filter without this returns far fewer results.

Location

ParameterTypeDescription
job_country_code_orstring[]ISO country codes to include, e.g. ["US","GB"].
job_country_code_notstring[]ISO country codes to exclude, e.g. ["IN"].
job_location_orstring[]City or region contains any of these, e.g. ["Seattle","WA"]. Terms of three characters or fewer match whole values (WA is Washington, never Iowa). Combine with job_country_code_or to disambiguate same-named cities.
region_orstring[]US states and Canadian provinces as ISO 3166-2 codes, e.g. ["US-NY","CA-ON"]. More precise than job_location_or for a state or province.
metro_code_orstring[]US CBSA metro codes, e.g. "35620" (New York). Non-US jobs never match.
remotebooleantrue returns remote-only, false excludes remote.
work_arrangement_orstring[]remote, hybrid or onsite — finer than remote, which reads false for hybrid and onsite alike. Jobs with an unknown arrangement never match.

Source

ParameterTypeDescription
source_orstring[]Match any collector source, e.g. ["linkedin","greenhouse"]. Case, spaces and punctuation are ignored; yc is an alias for ycombinator.
source_notstring[]Exclude sources. ["indeed","linkedin"] drops the two largest boards; pin the ATS list in source_or for ATS-only.

Role and classification

ParameterTypeDescription
employment_type_orstring[]full-time, part-time, contract, temporary, internship.
include_unlabeled_employment_typebooleanAlso return jobs with no employment type (about 27% of postings).
job_seniority_orstring[]Seniority levels to include.
include_unlabeled_senioritybooleanAlso return jobs with no seniority (about 55% of postings).
skills_orstring[]Skill slugs, e.g. ["python","kubernetes"].
esco_skill_id_orstring[]ESCO skill concept IDs (exact match).
occupation_code_orstring[]ISCO-08 codes; 4 digits exact ("2512" Software Developers), 1-3 digits match as prefixes.
isic_division_orstring[]ISIC Rev.4 employer industry divisions, 2 digits ("62" Computer programming).

Pay, perks and posting signals

ParameterTypeDescription
min_salary_usdnumberPosted salary (top of range, annualized USD) reaches this amount. Jobs without a posted salary never match.
benefits_orstring[]Benefit slugs, e.g. ["401k","health insurance"]. Structured source data only, so coverage is partial.
visa_sponsorship_orstring[]offers, no or citizenship_required, parsed from the posting text. Jobs that say nothing never match.
has_recruiter_emailbooleantrue for only jobs with a parsed recruiter email, false for only jobs without one.
max_applicant_countnumberAt most this many applicants. Only LinkedIn exposes counts, so jobs without one are dropped.
max_ghost_scorenumberExclude jobs whose ghost-likelihood score (0-100) exceeds this. Unscored jobs pass.

Dates, paging and output

ParameterTypeDescription
posted_at_gtestringOnly postings on or after this date (YYYY-MM-DD).
posted_at_ltestringOnly postings on or before this date (YYYY-MM-DD).
posted_at_max_age_daysnumberOnly postings newer than this many days.
last_verified_max_age_daysnumberOnly postings we last confirmed live on their source within this many days.
statusstringactive (the default), closed, or any. Closed postings keep their data and are how a past hiring question is answered.
include_unknownstring[]Field names whose unlabelled jobs that field's filter should keep rather than drop, e.g. ["language"].
limitnumberRows to return. Defaults to 25 and is clamped to your plan's max results per call (25 on Free).
offsetnumberRows to skip, for paging.
cursorstringContinue an earlier search from metadata.next_cursor.
detailstringcompact (the default) or full. The tool's own argument, not a search filter.
include_total_resultsbooleanPopulate metadata.total_results (slightly slower).
blur_company_databooleanDeprecated and ignored. Preview mode has been removed; every search returns the full record and is billed per job.

About 17% of postings carry an arrangement, so work_arrangement_or returns a real but partial slice and silently drops the rest. Use remote for breadth, work_arrangement_or when hybrid and onsite must be told apart.

A typical agent call:

{
  "job_title_or": ["data engineer"],
  "remote": true,
  "posted_at_max_age_days": 14,
  "limit": 25,
  "include_total_results": true
}

The response mirrors the REST API: a metadata block (with total_results when requested) and a data array of normalized postings — title, company, location, country, salary range, seniority, posting date, and the apply URL. Each posting also carries sources (every board it was seen on, not just the first), last_seen_at and verified_at (when a recheck last confirmed the posting was live, and when we last looked at all), and is_manager and job_function where they are known — is_manager is what separates a principal individual contributor from an actual director, since seniority files both under "director". See the job schema for the full shape and for how much of the corpus carries each field.

How much of each posting you get. Rows come back compact by default: the fields a list of results is read for, with the description cut to a snippet and anything empty left out. A compact row says description_truncated: true and description_chars when it cut one, so you know there is more to read. Read one posting in full with fetch, or pass detail: "full" to get every field of every row exactly as the REST API answers it.

Paging. Page with cursor: take metadata.next_cursor from a response and send it back as cursor on the next call, unchanged, with the same filters. Stop when a page comes back with no next_cursor. offset still works and is simpler for a handful of pages, but a cursor is steadier on a corpus that keeps growing and is the only way past the offset ceiling. Because limit is clamped silently to your plan's page size, a page shorter than the limit you asked for does not mean you reached the end.

What a call cost. metadata.credits_charged is what the call used and metadata.jobs_already_paid is how many rows were free because your account already paid for them this calendar month. Both are absent on an account that is not billed per job.

create_signal and list_signals

A signal is a saved search that tells you when something new matches it, so an agent does not have to re-run the same search on a timer. Matches are keyed on the first time a posting entered the corpus, so a repost or a backfill does not fire again, and evaluating a signal costs no job credits.

create_signal takes:

ParameterTypeDescription
namestringA short label for this signal.
filtersobjectThe search that defines a match, in the shape search_jobs takes, minus paging and anything that decides what counts as new — the signal keeps its own watermark.
destinationsobject[]Where matches go: { kind: "email" | "slack" | "webhook", target, cadence: "instant" | "daily" }. Email is available on every plan.
modestringjobs fires on each newly matching posting; companies (the default) fires the first time a company matches.
intentstringFree text describing what you are watching for.
idempotency_keystringYour own key for this signal. Send the same key on a retry and the signal already saved is replayed instead of a second one being created.

Run the same filters through search_jobs first to see what they return before saving them.

Both tools answer the same shape — create_signal one signal, list_signals an array of signals:

{
  "signal": {
    "id": "9f1c2f1e-...",
    "name": "Fintech hiring in Berlin",
    "mode": "jobs",
    "enabled": true,
    "filters": { "job_title_or": ["backend engineer"], "job_country_code_or": ["DE"] },
    "intent": "Berlin fintechs starting to hire backend engineers",
    "grade_leads": false,
    "created_at": "2026-09-22T10:00:00.000Z",
    "last_evaluated_at": null,
    "last_error": null,
    "consecutive_failures": 0,
    "destinations": [
      {
        "id": "7a2b3c4d-...",
        "kind": "webhook",
        "target": "https://example.com/hooks/jobspipe",
        "cadence": "instant",
        "enabled": true,
        "signing_secret": "whsec_..."
      }
    ]
  }
}

A webhook destination's signing_secret is returned once, when the signal is created — store it then, because a later list_signals does not include it. See Signals for how deliveries are signed and retried.

detect_company_tech_stack

ParameterTypeDescription
domainstringRequired. Domain to scan, e.g. "stripe.com". URLs and www. are normalized.
modestringauto (default) tries a fast HTTP fetch, then a headless render if results are thin. html or render forces one strategy.

Returns domain, scanned_at, http_status, and a detected array — each entry with slug, name, categories, confidence, version, website, saas, and oss. Results are cached for 14 days.

search_documentation

ParameterTypeDescription
querystringRequired, at least 2 characters. What to look up, e.g. "filter by salary" or "webhook signature".
limitnumberMaximum sections to return, an integer from 1 to 20. Default 5.

Returns the query and a results array of sections, each with title, heading, url, excerpt, and score.

list_pricing_plans

Takes no parameters. Returns currency (USD) and a plans array with each plan's name, monthlyPriceUsd, monthlyJobs, maxResultsPerRequest, and requestsPerSecond.

get_account_info

Takes no parameters. Returns the account the connection is signed in to: user_id, email, name, auth_type (oauth or api_key), plan, the month the figures cover, monthly_credits, credits_used, credits_remaining, extra_credits, max_results_per_request and requests_per_second. It costs no credits. It is the same response as GET /v1/account, and the quickest way to check which account an OAuth connection landed on, or how much is left before a large search.

Limits and errors

The MCP server inherits your plan's limits from the REST API. Only authentication failures come back as an HTTP status. Everything that happens inside a tool — including an exhausted quota or a rate limit — comes back as a normal tool result with isError: true and a JSON body such as { "error": "Monthly request quota exceeded" }, so the agent can read it and react.

WhereWhat you getMeaning
HTTP 401Connect with OAuth, or send a JobsPipe API key (jp_live_) ...No Authorization: Bearer or x-api-key credential was sent.
HTTP 401Access token is invalid or expired. Reconnect to continue.The bearer is not a jp_live_ key and not a live OAuth token.
Tool result, isError{ "error": "Invalid API key" }The jp_live_ key was not accepted by the API.
Tool result, isError{ "error": "Monthly request quota exceeded" }Your plan's monthly job quota is used up.
Tool result, isError{ "error": "Rate limit exceeded" }Per-second rate limit exceeded — retry after a second.

Call get_account_info to see your own plan, limits and remaining credits, and list_pricing_plans to see the quota, rate limit, and max results per call for each plan.

Other agent surfaces

Beyond MCP, JobsPipe is discoverable by autonomous agents:

[

Monitor hiring at target companies

Track new postings at a set of companies to spot hiring signals.

](https://docs.jobspipe.dev/guides/monitor-companies)[

Connect JobsPipe to your assistant

Step-by-step setup for every AI assistant that can talk to JobsPipe — Claude, ChatGPT, Gemini, Grok, Perplexity, Le Chat, Cursor and the coding agents — from the directory listing or as a custom MCP connector.

](https://docs.jobspipe.dev/ai-agents/connect)