mcp-lever
mcp-lever connects a chat client to public Lever job boards so you can find and inspect open roles across named companies without an API key.
Resolve company names to Lever site names on the global or EU instance, including whether a board publishes openings.
Search openings across one to 25 companies, with filters for keyword, location, team, department, commitment, workplace type, country, salary, currency, salary interval, recency, limit, and skip.
Read one opening in full, including advert text, named sections, salary details, and source URL.
List a company's filter wordings for teams, locations, and commitments so filters match Lever's exact vocabulary.
Return structured results with per-company statuses, job URLs, apply URLs, and notes about pagination or limits.
Allows searching and retrieving job postings from Aircall's Lever-hosted career page, including resolving the company site name, filtering by location, team, department, commitment, keyword, workplace type, country, and salary, and fetching full job details.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-leverfind software engineer jobs at Included Health"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-lever
Lever is recruiting software that thousands of companies use to run their hiring, and every customer gets a public job board that comes with it. Each board carries that company's open positions with their title, their location, the team and department they sit in, the commitment they ask for, the full advert, and the salary range where the company chose to publish one. Lever hosts one board per company, on either its global or its European instance, and publishes no index across them.
This server connects a chat client to those boards. You name the companies you are interested in, and it turns each name into the site name that addresses its board, searches their openings, filters them by location, team, workplace type, country, salary or how recently they were posted, reads one opening in full, and lists the wordings each company filters by. It needs no API key and no account.
Install
One-click install
Claude Code
claude mcp add lever -- npx -y mcp-leverClaude Desktop, Cursor, and any client using the standard config format
{
"mcpServers": {
"lever": {
"command": "npx",
"args": ["-y", "mcp-lever"]
}
}
}Node 24 or later is required, and no environment variable has to be set.
With Docker
{
"mcpServers": {
"lever": {
"command": "docker",
"args": ["run", "-i", "--rm", "ghcr.io/smeet666/mcp-lever:2.1.0"]
}
}
}-i keeps stdin open, which is where the protocol travels, and -t is left out
because a TTY rewrites the stream. The container needs outbound HTTPS to
api.lever.co and api.eu.lever.co, and nothing else: no volume, no port, no
credential.
Bundle, without npm
Download mcp-lever-2.1.0.mcpb from
the latest release and
open it. A client that supports MCP bundles installs it on its own, with no npm
and no configuration file to edit. The bundle carries its dependencies, so
nothing is fetched at install time.
Related MCP server: RecruitData
What you can ask
"Which of Included Health, Netlify and Ramp are hiring on Lever?"
"Find me remote engineering roles at those three companies."
"Read me that opening in full."
"What locations does Included Health list its jobs under?"
"Anything posted in the last two weeks at Netlify?"
Every question starts from a company, since Lever offers no search across boards.
search_jobs resolves the names you give it, so no preparation is needed:
resolve_company(["Included Health"]) -> includedhealth, global instance, publishing
search_jobs(["Included Health"], keyword: "therapist")
get_job("includedhealth", "6f97a19f-…")Tools
Tool | What it does |
| Turns company names into the Lever site names of their boards. |
| Searches the openings of the companies you name. |
| Reads one opening in full, advert included. |
| Lists the wordings one company files its openings under. |
A Lever site name distinguishes case, so Flex answers where flex returns
nothing. Four spellings are tried per name on each of the two instances, and the
answer lists what was sent, so nothing found is never proof that a company is
absent from Lever.
resolve_company
Turns company names into Lever site names, reporting every instance that answered. It takes a list.
Argument | Type | Required | What it does |
| array of 1 to 25 strings | yes | Company names, or Lever site names you already know. |
In return: one entry per name, carrying input; found, a list of
{ slug, instance, publishes } where publishes is false for a site that exists
and lists nothing today; tried, the spellings sent in order; and cached, true
when this session had already resolved that name. A name answering on both
instances comes back with both, and neither is elected: pass the one you mean to
the other tools.
search_jobs
Searches the openings of the companies named. Lever applies the filters it supports on its own exact wording, and this server applies the rest to the openings it read.
Argument | Type | Required | What it does |
| array of 1 to 25 strings | yes | Company names or Lever site names. Each is resolved here. |
| string | no | Words to look for in the title and the advert. |
| array of 1 to 20 strings | no | Locations, exactly as Lever writes them. |
| array of 1 to 20 strings | no | Teams, exactly as Lever writes them. |
| array of 1 to 20 strings | no | Departments, exactly as Lever writes them. |
| array of 1 to 20 strings | no | Commitments, exactly as Lever writes them. |
| array of 1 to 4 strings | no |
|
| array of 1 to 20 two-letter codes | no | Countries as ISO codes, as in |
| number, 0 or more | no | The lowest upper bound of a salary range to keep. |
| string | no | The period |
| three-letter code | no | The currency |
| integer, 1 to 3650 | no | How recent an opening must be. |
| integer, 1 to 100, default | no | Openings to read per company. |
| integer, 0 to 100000, default | no | Openings to step over per company. |
Lever itself applies location, team, department and commitment; this
server applies keyword, workplace_type, country, salary_min,
salary_interval, currency and posted_within_days to what it read.
list_filter_values publishes the wordings the first four take, and a wording
Lever does not know comes back as an empty list.
In return: jobs, each carrying id and company_slug, which get_job
takes, plus title, location, all_locations, country, workplace_type,
team, posted_at, url and apply_url. commitment and department are
absent when the company records neither. salary is null for an opening
published without one, which is never the same as zero, and it carries the
interval Lever wrote it in, never converted or annualised. per_company gives
one outcome per company, with a status of read, unresolved, empty or
failed, which are four different answers, and the read and returned counts
around the filters. total_available is always null: Lever publishes no result
count. The rows carry no advert text, since one company's board can run to
megabytes.
limit applies per company, and a company whose openings fill it may publish
more: the notes say when that happened, and that a count taken inside that window
measures the window. posted_within_days walks up to five pages per company,
and Lever pages by title, so an opening published yesterday can sit anywhere in a
board.
get_job
Reads one opening in full: the advert, its named sections, and the salary as published.
Argument | Type | Required | What it does |
| string | yes | The Lever site name, as |
| string | yes | The identifier of one opening, as a search returns it. |
|
| no | The instance the row came from. The global one by default. |
In return: job, holding the fields a search row carries, plus
description, sections as { heading, items }, salary_note for what the
company wrote beside the range, and source with the address it was retrieved
from.
list_filter_values
Lists the team, location and commitment wordings one company uses. Read it before
filtering: Lever matches its own wording, and the vocabulary belongs to each
company, one writing Full-time where another writes EE Full-Time.
Argument | Type | Required | What it does |
| string | yes | The Lever site name, as |
|
| no | The instance this site lives on. The global one by default. |
| array of 1 to 3 of | no | Which vocabularies to read. Each costs one request, and all three are read by default. |
In return: company_slug, instance, and fields holding a list of
{ value, count } for each vocabulary asked for. A count is null where Lever
published no figure alongside the category.
Configuration
Nothing has to be configured. The server reads no environment variable, and the
mcpServers block above is complete as written.
The pacing, the timeout and the cache are settings of the client layer, which As a library shows how to pass. The interval between two requests can be widened there and never narrowed.
Errors
Every failure carries one of six codes, a message, and where it helps the values that would have been accepted.
Code | What happened | What to do |
| Lever answered, and holds no such site or opening. | Check the site name with |
| The arguments were refused before any request went out. | Read the message, which names the argument and what it takes. |
| Lever asked this client to slow down. | Wait, then call again with the same arguments. The opening is still on the board. |
| Lever answered in a shape this client cannot read. | Report it at the issue tracker. |
| The request did not complete. | Try again shortly. |
| The request passed its deadline. | Ask for fewer companies, or a smaller |
As a library
The layer reading Lever is published on its own, with its pacing, its cache and its errors, and with no protocol attached.
import { Client } from "mcp-lever/client";
const client = new Client({ minIntervalMs: 2000 });
const resolved = await client.resolveCompany("Included Health");
const jobs = await client.listPostings(resolved.found[0], { limit: 10 });
console.log(jobs.length);ClientOptions takes minIntervalMs, timeoutMs, cacheTtlMs and fetchImpl.
An interval below the published floor is ignored, so the floor holds here as
well.
Pacing and attribution
Both API hosts publish Crawl-delay: 1, so requests go out one at a time with at
least a second between them, and that floor holds however the client is
configured. The User-Agent carries the project and an address where a person
can be reached, and imitates no browser.
Reads go to api.lever.co and api.eu.lever.co, which are the hosts Lever
documents for its posting data. The jobs.lever.co careers pages are left alone.
Every opening carries the address of its Lever page and its apply URL. Credit the company and link that page when you show an opening.
This MCP server is an unofficial project, with no affiliation to Lever or to the companies whose boards it reads.
Privacy
This server collects nothing about you and sends nothing to its author. It runs
on your machine, contacts api.lever.co and api.eu.lever.co and nothing else, holds its answers in memory
while it runs, and writes nothing to disk.
PRIVACY.md states what a request carries and which settings change
any of it.
Development
npm install
npm run build:fixtures
npm test
npm run checkTests run against generated fixtures and make no network request. The live suite,
npm run test:live, makes one request per route and runs nightly against the
service itself.
Contributing
Bugs, questions and ideas belong in the issue tracker. Pull requests are welcome; opening an issue first helps agree on the shape of the change. See CONTRIBUTING.md.
License
MIT, see LICENSE. The openings belong to the companies that published them.
mcp-lever (français)
Lever est un logiciel de recrutement qu'utilisent des milliers d'entreprises pour mener leurs embauches, et chaque cliente reçoit avec lui un site d'offres public. Chaque site porte les postes ouverts de cette entreprise avec leur intitulé, leur lieu, l'équipe et le département auxquels ils appartiennent, le type de contrat demandé, l'annonce complète, et la fourchette de salaire quand l'entreprise a choisi d'en publier une. Lever héberge un site par entreprise, sur son instance mondiale ou sur son instance européenne, et ne publie aucun index les traversant.
Ce serveur relie un client de conversation à ces sites. Vous nommez les entreprises qui vous intéressent, et il traduit chaque nom en l'identifiant qui adresse son site, cherche dans leurs offres, les filtre par lieu, équipe, mode de travail, pays, salaire ou fraîcheur de publication, lit une offre en entier, et liste les formulations selon lesquelles chaque entreprise classe les siennes. Aucune clé d'API, aucun compte.
Installation
Installation en un clic
Claude Code
claude mcp add lever -- npx -y mcp-leverClaude Desktop, Cursor, et tout client au format de configuration standard
{
"mcpServers": {
"lever": {
"command": "npx",
"args": ["-y", "mcp-lever"]
}
}
}Node 24 ou plus récent est nécessaire, et aucune variable d'environnement n'est à renseigner.
Avec Docker
{
"mcpServers": {
"lever": {
"command": "docker",
"args": ["run", "-i", "--rm", "ghcr.io/smeet666/mcp-lever:2.1.0"]
}
}
}-i garde l'entrée standard ouverte, qui est le canal du protocole, et -t est
omis parce qu'un TTY réécrit le flux. Le conteneur a besoin d'un accès HTTPS
sortant vers api.lever.co et api.eu.lever.co, et de rien d'autre : aucun
volume, aucun port, aucun identifiant.
Bundle, sans npm
Téléchargez mcp-lever-2.1.0.mcpb depuis
la dernière publication
et ouvrez-le. Un client qui gère les bundles MCP l'installe seul, sans npm et
sans fichier de configuration à modifier. Le bundle emporte ses dépendances, donc
rien n'est téléchargé à l'installation.
Ce qu'on peut demander
« Lesquelles d'Included Health, Netlify et Ramp recrutent sur Lever ? »
« Trouve-moi des postes d'ingénierie en télétravail chez ces trois-là. »
« Lis-moi cette offre en entier. »
« Sous quels lieux Included Health classe-t-elle ses offres ? »
« Quelque chose publié ces quinze derniers jours chez Netlify ? »
Chaque question part d'une entreprise, puisque Lever n'offre aucune recherche
traversant les sites. search_jobs résout lui-même les noms qu'on lui donne,
donc rien n'est à préparer :
resolve_company(["Included Health"]) -> includedhealth, instance mondiale, publie
search_jobs(["Included Health"], keyword: "therapist")
get_job("includedhealth", "6f97a19f-…")Les outils
Outil | Ce qu'il fait |
| Traduit des noms d'entreprises en identifiants de sites Lever. |
| Cherche dans les offres des entreprises nommées. |
| Lit une offre en entier, annonce comprise. |
| Liste les formulations sous lesquelles une entreprise classe. |
Un identifiant de site Lever distingue la casse, donc Flex répond là où flex
ne rend rien. Quatre orthographes sont essayées par nom sur chacune des deux
instances, et la réponse liste ce qui a été envoyé : ne rien trouver ne prouve
jamais qu'une entreprise est absente de Lever.
resolve_company
Traduit des noms d'entreprises en identifiants de sites Lever, en signalant chaque instance qui a répondu. Il prend une liste.
Argument | Type | Requis | Ce qu'il fait |
| tableau de 1 à 25 chaînes | oui | Des noms d'entreprises, ou des identifiants déjà connus. |
En retour : une entrée par nom, portant input ; found, une liste de
{ slug, instance, publishes } où publishes est faux pour un site qui existe
et ne liste rien aujourd'hui ; tried, les orthographes envoyées dans l'ordre ;
et cached, vrai quand la session avait déjà résolu ce nom. Un nom qui répond
sur les deux instances revient avec les deux, et aucune n'est élue : passez celle
que vous visez aux autres outils.
search_jobs
Cherche dans les offres des entreprises nommées. Lever applique les filtres qu'il gère sur sa propre formulation exacte, et ce serveur applique les autres aux offres qu'il a lues.
Argument | Type | Requis | Ce qu'il fait |
| tableau de 1 à 25 chaînes | oui | Noms d'entreprises ou identifiants. Chacun est résolu ici. |
| chaîne | non | Mots à chercher dans l'intitulé et dans l'annonce. |
| tableau de 1 à 20 chaînes | non | Des lieux, exactement comme Lever les écrit. |
| tableau de 1 à 20 chaînes | non | Des équipes, exactement comme Lever les écrit. |
| tableau de 1 à 20 chaînes | non | Des départements, exactement comme Lever les écrit. |
| tableau de 1 à 20 chaînes | non | Des types de contrat, exactement comme Lever les écrit. |
| tableau de 1 à 4 chaînes | non |
|
| tableau de 1 à 20 codes à deux lettres | non | Des pays en code ISO, comme |
| nombre, 0 ou plus | non | La plus basse borne haute de fourchette à conserver. |
| chaîne | non | La période dans laquelle |
| code à trois lettres | non | La devise dans laquelle |
| entier, 1 à 3650 | non | L'ancienneté maximale d'une offre. |
| entier, 1 à 100, défaut | non | Offres à lire par entreprise. |
| entier, 0 à 100000, défaut | non | Offres à enjamber par entreprise. |
Lever applique lui-même location, team, department et commitment ; ce
serveur applique keyword, workplace_type, country, salary_min,
salary_interval, currency et posted_within_days à ce qu'il a lu.
list_filter_values publie les formulations que prennent les quatre premiers, et
une formulation que Lever ignore revient en liste vide.
En retour : jobs, chacune portant id et company_slug, que get_job
reprend, plus title, location, all_locations, country, workplace_type,
team, posted_at, url et apply_url. commitment et department sont
absents quand l'entreprise ne les renseigne pas. salary vaut null pour une
offre publiée sans fourchette, ce qui ne vaut jamais zéro, et porte l'interval
dans lequel Lever l'a écrite, jamais converti ni annualisé. per_company donne
une issue par entreprise, avec un status valant read, unresolved, empty
ou failed, qui sont quatre réponses différentes, et les comptes read et
returned de part et d'autre des filtres. total_available vaut toujours
null : Lever ne publie aucun compte de résultats. Les lignes ne portent pas
l'annonce, un site d'entreprise pouvant peser plusieurs mégaoctets.
limit s'applique par entreprise, et une entreprise dont les offres le
remplissent en publie peut-être davantage : les notes le signalent, et disent
qu'un compte pris dans cette fenêtre mesure la fenêtre. posted_within_days
parcourt jusqu'à cinq pages par entreprise, et Lever pagine par intitulé, donc
une offre publiée hier peut se trouver n'importe où dans un site.
get_job
Lit une offre en entier : l'annonce, ses sections nommées, et le salaire tel que publié.
Argument | Type | Requis | Ce qu'il fait |
| chaîne | oui | L'identifiant du site, rendu par |
| chaîne | oui | L'identifiant d'une offre, rendu par une recherche. |
|
| non | L'instance d'où vient la ligne. La mondiale par défaut. |
En retour : job, qui porte les champs d'une ligne de recherche, plus
description, sections en { heading, items }, salary_note pour ce que
l'entreprise a écrit à côté de la fourchette, et source avec l'adresse d'où
l'offre a été lue.
list_filter_values
Liste les formulations d'équipe, de lieu et de contrat qu'une entreprise emploie.
À lire avant de filtrer : Lever fait correspondre sa propre formulation, et le
vocabulaire appartient à chaque entreprise, l'une écrivant Full-time là où une
autre écrit EE Full-Time.
Argument | Type | Requis | Ce qu'il fait |
| chaîne | oui | L'identifiant du site, rendu par |
|
| non | L'instance où vit ce site. La mondiale par défaut. |
| tableau de 1 à 3 parmi | non | Les vocabulaires à lire. Chacun coûte une requête, et les trois sont lus par défaut. |
En retour : company_slug, instance, et fields qui porte une liste de
{ value, count } pour chaque vocabulaire demandé. Un count vaut null
là où Lever n'a publié aucun chiffre à côté de la catégorie.
Configuration
Il n'y a rien à configurer. Le serveur ne lit aucune variable d'environnement, et
le bloc mcpServers ci-dessus est complet tel quel.
Le rythme, le délai et le cache sont des réglages de la couche cliente, que Comme bibliothèque montre comment passer. L'écart entre deux requêtes peut y être élargi et jamais resserré.
Erreurs
Chaque échec porte un des six codes, un message, et quand cela aide les valeurs qui auraient été acceptées.
Code | Ce qui s'est passé | Que faire |
| Lever a répondu, et n'a ni ce site ni cette offre. | Vérifiez l'identifiant avec |
| Les arguments ont été refusés avant toute requête. | Lisez le message, qui nomme l'argument et ce qu'il prend. |
| Lever demande à ce client de ralentir. | Attendez, puis rappelez avec les mêmes arguments. L'offre est toujours en ligne. |
| Lever a répondu dans une forme que ce client ne lit pas. | Signalez-le sur le suivi d'incidents. |
| La requête n'a pas abouti. | Réessayez sous peu. |
| La requête a dépassé son délai. | Demandez moins d'entreprises, ou un |
Comme bibliothèque
La couche qui lit Lever est publiée seule, avec son rythme, son cache et ses erreurs, sans protocole attaché.
import { Client } from "mcp-lever/client";
const client = new Client({ minIntervalMs: 2000 });
const resolved = await client.resolveCompany("Included Health");
const jobs = await client.listPostings(resolved.found[0], { limit: 10 });
console.log(jobs.length);ClientOptions prend minIntervalMs, timeoutMs, cacheTtlMs et fetchImpl.
Un écart sous le plancher publié est ignoré, donc le plancher tient également
ici.
Rythme et attribution
Les deux hôtes d'API publient Crawl-delay: 1, donc les requêtes partent une à
une avec au moins une seconde entre elles, et ce plancher tient quelle que soit
la configuration du client. Le User-Agent porte le projet et une adresse où
joindre une personne, et n'imite aucun navigateur.
Les lectures vont vers api.lever.co et api.eu.lever.co, les hôtes que Lever
documente pour ses données d'offres. Les pages carrières jobs.lever.co sont
laissées tranquilles.
Chaque offre porte l'adresse de sa page Lever et son adresse de candidature. Créditez l'entreprise et renvoyez vers cette page quand vous montrez une offre.
Ce MCP est un projet non officiel, sans affiliation à Lever ni aux entreprises dont il lit les sites.
Confidentialité
Ce serveur ne collecte rien sur vous et n'envoie rien à son auteur. Il tourne sur
votre machine, ne joint que api.lever.co et api.eu.lever.co, garde ses réponses en mémoire le temps qu'il
tourne, et n'écrit rien sur le disque. PRIVACY.md dit ce qu'une
requête emporte et quels réglages changent cela.
Développement
npm install
npm run build:fixtures
npm test
npm run checkLes tests s'exécutent sur des fixtures engendrées et n'émettent aucune requête.
La suite en direct, npm run test:live, émet une requête par route et tourne
chaque nuit contre le service lui-même.
Contribuer
Les anomalies, les questions et les idées ont leur place dans le suivi d'incidents. Les propositions de modification sont bienvenues ; ouvrir un ticket d'abord aide à s'accorder sur la forme du changement. Voir CONTRIBUTING.md.
Licence
MIT, voir LICENSE. Les offres appartiennent aux entreprises qui les ont publiées.
Available Tools
4 toolsget_jobRead one openingARead-onlyIdempotent
Read one Lever opening in full: the advert, its named sections, and the salary as published. company_slug and instance are the ones the row carried, and a site living on both instances holds different openings on each.
| Name | Required | Description | Default |
|---|---|---|---|
| job_id | Yes | The identifier of one opening, as a search returns it. | |
| instance | No | The Lever instance the row came from. Left out, the global one is read. | |
| company_slug | Yes | The Lever site name, as resolve_company returns it. |
Output Schema
| Name | Required | Description |
|---|---|---|
| job | Yes | |
| notes | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare read-only, idempotent, open-world, and non-destructive behavior. The description adds meaningful context beyond that: it describes what the read returns and warns that a site present on both instances has different openings on each, which prevents cross-instance mistakes.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two focused sentences with no filler. The core purpose is front-loaded, and the instance distinction is stated compactly afterward.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
With annotations covering safety hints and an output schema covering return shape, the description supplies enough to call this correctly, including the key instance caveat. Minor omissions such as not-found behavior or explicit routing from search_jobs are not critical for a simple read tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already defines all three parameters. The description still adds semantic nuance by explaining that company_slug and instance should match the originating row and that the same site can have distinct openings per instance, which is not captured in the schema alone.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description names a specific verb and resource: reading one Lever opening in full. It also enumerates the returned content (advert, named sections, published salary), which distinguishes this from search_jobs and makes the tool's purpose unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It implies use after obtaining a row from search or resolution by saying company_slug and instance are those the row carried, but it never explicitly names search_jobs or states when not to use this tool. The instance caveat is useful, but alternative-routing guidance remains implicit.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_filter_valuesList a company's filter wordingsARead-onlyIdempotent
List the team, location and commitment wordings one company uses on Lever, so a filter can carry the words that company publishes. The vocabulary belongs to each company: one writes Full-time, another Full Time, another EE Full-Time. Lever publishes no vocabulary for department, so a department filter is checked against the openings a company has open right now.
| Name | Required | Description | Default |
|---|---|---|---|
| fields | No | Which vocabularies to read. Each costs one request. All three by default. | |
| instance | No | The Lever instance this site lives on. Left out, the global one is read. | |
| company_slug | Yes | The Lever site name, as resolve_company returns it. |
Output Schema
| Name | Required | Description |
|---|---|---|
| notes | Yes | |
| fields | Yes | |
| instance | Yes | |
| company_slug | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover readOnlyHint, openWorldHint, idempotentHint, and destructiveHint. The description adds meaningful behavioral context: vocabulary is company-specific (e.g., 'Full-time' vs 'Full Time') and department filters are checked against current openings because no vocabulary is published. This goes beyond the annotations and helps the agent understand the data's nature.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two sentences, front-loaded with the action, and efficiently explains the tool's purpose, the per-company variation, and the department exception. Every sentence adds valuable context without redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple read-only tool with an output schema present, the description sufficiently covers what the tool does, why it is used, and an important edge case (department). The combination of annotations, schema descriptions, and the description gives an agent everything needed to select and invoke the tool correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema describes all parameters with 100% coverage, including defaults (fields defaults to all three, instance defaults to global, company_slug is required). The description underscores the purpose of fields (team, location, commitment) but adds no new parameter-level syntax or details. Baseline 3 is appropriate when schema carries the semantic load.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's function: 'List the team, location and commitment wordings one company uses on Lever.' It names the specific verb ('List'), the resource (filter wordings for a company), and the scope (team, location, commitment), distinguishing it from sibling tools like search_jobs and get_job.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explains when this tool is valuable: when building filters that need company-specific wording ('so a filter can carry the words that company publishes'). It also provides context by noting that department vocabulary is not published and is handled differently, implicitly guiding the agent away from expecting it. It does not explicitly name alternatives but the use case is clear.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
resolve_companyResolve company names to Lever boardsARead-onlyIdempotent
Turn company names into the Lever site names that address their job boards, and report every instance that answered. search_jobs resolves names on its own, so reach for this to check which of several companies are on Lever at all: it probes names without reading a single board, where a search would read one per company.
| Name | Required | Description | Default |
|---|---|---|---|
| names | Yes | A list of a company name, or a Lever site name you already know, one to 25 of them. |
Output Schema
| Name | Required | Description |
|---|---|---|
| notes | Yes | |
| resolved | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, openWorldHint, idempotentHint, and destructiveHint. The description adds valuable context: it reports every instance that answered and probes names without reading a single board, clarifying the operational behavior and lightweight nature beyond the annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is two sentences, with the first sentence stating the core function and the second providing usage guidance. Every sentence contributes meaning, with no fluff or repetition of structured data.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the simple parameter schema, comprehensive annotations, and presence of an output schema, the description covers all necessary aspects: purpose, usage differentiation, and behavioral traits. It does not need to explain return values because an output schema exists.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The single parameter 'names' is fully described in the schema (100% coverage), so the description does not need to add parameter details. It aligns with the schema by referring to 'company names' and 'Lever site names', but adds no additional syntax or format information, matching the baseline for full schema coverage.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description uses a specific verb ('turn') and resource ('company names' to 'Lever site names'), clearly stating the mapping action and output. It also distinguishes itself from search_jobs by noting that this tool checks which companies are on Lever without reading boards.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly states when to use this tool versus search_jobs: search_jobs resolves names on its own, while this tool is for checking which of several companies are on Lever. It also explains the efficiency advantage (probes without reading boards).
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_jobsSearch openings at named companiesARead-onlyIdempotent
Search the openings published by named companies on Lever. companies takes company names or Lever site names, and each name is turned into a site name here. A name found on the first spelling costs 3 requests, and one that resists costs up to 9, at one second each. location, team, department and commitment are sent to Lever and need its exact wording. keyword, workplace_type, country, salary and recency are applied here, because Lever accepts none of them as filters. limit applies per company, so a company whose openings fill it may publish more. posted_within_days walks up to 5 pages per company, because Lever pages by title and a recent opening sits anywhere.
| Name | Required | Description | Default |
|---|---|---|---|
| skip | No | How many openings to step over per company. | |
| team | No | A list of a team exactly as Lever writes it, one to 20 of them. | |
| limit | No | How many openings to read per company. | |
| country | No | A list of a two-letter country code, as in FR or US, one to 20 of them. | |
| keyword | No | Words to look for in the title and the advert. | |
| currency | No | A three-letter currency code, as in EUR or USD. | |
| location | No | A list of a location exactly as Lever writes it, one to 20 of them. | |
| companies | Yes | A list of a company name or a Lever site name, one to 25 of them. | |
| commitment | No | A list of a commitment exactly as Lever writes it, one to 20 of them. | |
| department | No | A list of a department exactly as Lever writes it, one to 20 of them. | |
| salary_min | No | The lowest upper bound of a salary range to keep. | |
| workplace_type | No | A list of remote, hybrid, onsite or unspecified, one to 4 of them. | |
| salary_interval | No | The period salary_min is written in, as Lever writes it, such as per-year-salary or per-hour-wage. | |
| posted_within_days | No | How recent an opening must be. |
Output Schema
| Name | Required | Description |
|---|---|---|
| jobs | Yes | |
| notes | Yes | |
| per_company | Yes | |
| total_available | Yes | Lever publishes no result count, so this is always null. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Goes well beyond the annotations by disclosing request cost (3 requests for a clean company name, up to 9 at one second each for a stubborn one), per-company limit semantics (a company that fills the limit may publish more), and the up-to-5-page walk for posted_within_days because Lever pages by title. These are exactly the operational traits an agent needs before committing to a call.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the purpose sentence, then each subsequent sentence attaches to a specific concern (companies mapping, cost, server-vs-local filters, limit, recency paging). No filler and no repetition of the schema text.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a 14-parameter tool with an output schema, it covers the hard parts an agent would otherwise get wrong: cost, filter routing, per-company limits and the recency page walk. Minor gaps remain around how skip interacts with per-company paging and what happens when a company name cannot be resolved.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3; the description earns above that by adding semantics the schema does not carry: company names are converted to Lever site names internally, and location/team/department/commitment are server-side while keyword, workplace_type, country, salary and recency are filtered locally. It leaves skip and salary_min/salary_interval to the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource: it searches openings published by named companies on Lever, with a clear companies-driven scope. It is distinguishable from get_job (single opening) and resolve_company (name resolution), though it does not name those siblings to draw the boundary explicitly.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explains which filters Lever accepts versus which are applied locally, and warns about per-company cost, which is useful invocation guidance. However, it never says when to use this versus resolve_company or list_filter_values, and the 'need its exact wording' note would be stronger if it pointed at list_filter_values as the way to obtain that wording.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
2 tool updates
v2.1.0- Changed
get_job4 fields changed- removed
Output schema / properties / job / properties / country / anyOfRemoved value: -[ - { - "type": "string" - }, - { - "type": "null" - } -] - added
Output schema / properties / job / properties / country / typeAdded value: +[ + "string", + "null" +] - removed
Output schema / properties / job / properties / salary_note / anyOfRemoved value: -[ - { - "type": "string" - }, - { - "type": "null" - } -] - added
Output schema / properties / job / properties / salary_note / typeAdded value: +[ + "string", + "null" +]
- Changed
search_jobs4 fields changed- removed
Output schema / properties / jobs / items / properties / country / anyOfRemoved value: -[ - { - "type": "string" - }, - { - "type": "null" - } -] - added
Output schema / properties / jobs / items / properties / country / typeAdded value: +[ + "string", + "null" +] - removed
Output schema / properties / per_company / items / properties / slug / anyOfRemoved value: -[ - { - "type": "string" - }, - { - "type": "null" - } -] - added
Output schema / properties / per_company / items / properties / slug / typeAdded value: +[ + "string", + "null" +]
4 tool updates
v1.0.0- First observed
get_job - First observed
list_filter_values - First observed
resolve_company - First observed
search_jobs
TDQS
Scored across 4 tools
search_jobs and resolve_company share the company-name-resolution step, but the descriptions clearly separate probing which companies are on Lever from actually reading job boards. get_job and list_filter_values have distinct purposes. There is still a small moment of hesitation over whether to call resolve_company first, but otherwise the tools are easy to tell apart.
All four tools use a consistent snake_case verb_noun pattern: search_jobs, resolve_company, get_job, list_filter_values. The convention is predictable and readable throughout.
Four tools is a well-scoped set for a read-only Lever job-search server. Each tool earns its place: search, company resolution, job detail, and filter vocabulary lookup.
The read-only lifecycle is covered: resolving company names, searching openings, reading a full job, and listing filter vocabularies. Minor gaps remain, such as no direct department-vocabulary listing and no way to browse all Lever companies without naming them, but agents can work around these.
Maintenance
Related MCP Connectors
Public MCP server for discovering open jobs. Search, filter, and get application links.
GetJobzi MCP server for job search, application tracking, and career forecasting.
Greenhouse/Lever jobs via Apify MCP; needs your Apify token. Unofficial; Ondalith is AI-operated.
A jobs data API and MCP server. Every listing comes from the employer's own career site, an ATS feed or a government employment service, never resold from another board. Each row names its source and when we last confirmed the vacancy existed, and a role that disappears keeps a closure date and a reason. Six tools: search_jobs, get_job, company_hiring, market_signals, coverage, changes_since.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceMCP server that exposes job search data from multiple boards, enabling clients to query and manage job listings via natural language.7MIT
- AlicenseNot gradedqualityDmaintenanceUnified job search MCP server that aggregates live listings from multiple job boards with deduplication, enabling AI agents to find and filter jobs by keyword and location.MIT
- AlicenseAqualityDmaintenanceMCP server that scours job openings from public, ToS-clean sources (Greenhouse, Lever, Ashby, HN, RemoteOK, Adzuna, USAJobs) and provides tools for job search, company listings, and salary context.4MIT
- AlicenseNot gradedqualityDmaintenanceMCP server that aggregates and deduplicates job listings from multiple public sources, ranks them against a user's resume, and exposes tools for searching, viewing details, explaining fit, and tracking applications.MIT