MCP tool reference
What an AI assistant connected to First can actually do — every tool it can call, and what each one changes.
The First MCP server: connect an AI assistant to First to research a talent market, run the full role lifecycle (create, configure, edit the application process, publish, pause, close, archive), source candidates from the organisation's own candidate database and from the public web, and report on hiring.
Point your assistant at the First MCP server and sign in. Assistants that support it will sign you in with your First account; for tools that need a static credential instead, generate a personal API key in First under Profile, then Personal access tokens.
{
"mcpServers": {
"first": {
"url": "https://api.firststage.co/mcp",
"headers": { "Authorization": "Bearer fst_..." }
}
}
}Every tool below is authorised as you: it can only reach the organisations your account belongs to, roles you are allowed to see are the only ones it can read, and changing a role needs the same admin rights it would in the app. Tools marked CHANGES DATA alter or withdraw something people can see — a good assistant will ask you before running one.
How your assistant is told to use these tools▼
The server hands every assistant these instructions when it connects. They are worth reading: they are what makes an assistant source against agreed criteria rather than guessing, and they explain the sourcing workspace that ties a run of searches together.
First is a recruitment platform. These tools research the talent market, manage roles, source candidates from the organisation's own candidate database and from the public web, and report on hiring. Every tool but one takes an organisationId. Never ask the user for it — call list_organisations and use what comes back. Most people belong to exactly one organisation, so use it without checking; ask which they mean only when there are several. The hiring workflow, when someone is hiring rather than asking a one-off question. Offer it, do not force it — and never walk someone through it who only wanted a number or a database search: 1. RESEARCH the role and market — research_talent_market for demand, pay and who else is hiring. Then suggest a role, with their go-ahead before you create anything. 2. CAPTURE the intake — ask for the hiring manager's brief in whatever form it exists: notes, an interview transcript, a job description. There is no form to fill in, so take it as it comes. 3. GENERATE the criteria — draft the role from that brief and generate_suggested_criteria from it. These are the same criteria that later score applicants. 4. SOURCE candidates — search the database against those exact criteria, then present matches for the user to add. 5. PUBLISH — once they are happy, create the role, apply the same criteria, add the candidates and publish. Only then are those candidates assessed, against the very same criteria. The thread through 3, 4 and 5 is the point: one set of criteria, agreed once, used to source and then to assess. Steps can be skipped when the user already knows what they want, but do not skip the criteria. At step 4 there are two places to look for people, and they are alternatives rather than stages — say which one you are using: - The organisation's OWN database (source_candidates_from_draft_role). Fast, free, and everyone it returns has applied before, so First already holds their CV and assessment history. Look here first: re-engaging a known candidate beats cold-sourcing a stranger. - The PUBLIC WEB (search_web_for_candidates). Finds people who have never applied. Slower, quota-limited, and it saves nothing on its own — pick the profiles worth keeping and pass those to save_web_candidates, which enriches them into the same workspace the database searches use. Reach for it when the database is thin or the user asks for the web, LinkedIn, or "outside our database". The rest of this section is about the database search. The web search is driven by a query string instead of criteria, so it is described entirely in its own tool description. Sourcing the database always goes through source_candidates_from_draft_role — no role needs to exist. Never search the database on a job title alone. A title-only search matches anyone whose CV contains those words, so every candidate comes back looking strong and the band tells the user nothing. Every database search goes out with criteria behind it. Before the first search, gather two things — usually one question: - Seniority, or years of experience. - A few must-have skills or technologies. Then call generate_suggested_criteria and search with the result as criteriaChanges. That is the normal path, not the thorough one. If the user will not be drawn — "just search", "I don't know yet" — do not block them and do not fall back to a bare title. Infer sensible seniority and skills from the title and whatever context you have, pass your inference as the title and keywords, generate criteria from it, run the search, and say plainly what you assumed: "I've assumed senior-level and a TypeScript/React stack — tell me if that's wrong and I'll redo it." An assumption the user can correct beats a broad search they cannot interpret. How much the user invests changes only how good the criteria are: - FAST: seniority and a few skills, or your stated inference. - BETTER: a job description too, giving the generator far more to work with — and the criteria are the same set you later apply to the role, so the people you source are matched against the requirements they will later be assessed on. Show the criteria when the user is shaping the role; keep them in the background when they just want candidates. Either way, they exist. A match band is a fast skill-overlap signal, not a preview of the assessment score: sourcing compares a candidate's skills, past roles and previously-met criteria, while assessment evaluates their CV and application against every criterion. Present a band as "looks a strong fit to source", never as the score they will assess at. Each addition — more skills, a job description, refined criteria — improves the SAME search; you do not switch tools. Sharpening is always worth offering after the first page. Sourcing workspaces are a session you carry: - Omit `reference` on the first search. The response returns one — reuse that exact value for every later search in the same piece of hiring work, and pass it to add_sourced_candidates. - Reusing the reference is how you get MORE people: each search excludes candidates already saved to that workspace, so repeating it returns the next page. Inventing a new reference starts over and re-returns the same candidates. - A search only returns the page it sourced. get_sourced_candidates lists everything saved under a reference, so use it if you have lost track of earlier results or want to show the user everything found so far before adding anyone. Searching a location: 'radius' needs coordinates and 'country' needs an ISO code, so resolve the name first with geocode_place or geocode_country. If either returns found: false, fall back to searchType 'everywhere' rather than guessing coordinates. Once the user wants to commit: create_role, then update_role_process with the same criteria you sourced on, then add_sourced_candidates with the reference, then get_role to check readiness and publish_role. Sourced candidates are only assessed once the role is published. Before creating a role, gather what it needs: a title, a job description and at least one location are required to publish. Employment type, workplace type and compensation are not required but make the listing usable. Ask the user for anything missing rather than guessing.
The organisations a credential may act on — where the organisationId every other tool needs comes from.
list_organisationsList organisations
The organisations you can act on, with their ids. Every other tool needs an organisationId — call this first to find it rather than asking the user for one. Returns exactly one organisation for most people: use it without asking.
This tool takes no arguments.
Role lifecycle: create, configure, edit the application process, publish, pause, close, archive.
list_rolesList roles
List an organisation's roles, optionally filtered by status. Only returns roles the token owner is allowed to see. Use status 'imported' to find ATS-synced roles awaiting adoption.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| statuses | array of one of: imported, draft, open, paused, closed, archived |
get_roleGet a role
A role's current state: its details, criteria and questions, and readiness — which required details are still missing before it can be published.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| roleId* | string |
create_roleCreate a role
Create a new draft role. The token owner becomes the hiring manager. Location and category names that don't exist in the organisation are created automatically.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| name* | string | The role title |
| internalName | string | |
| shortDescription | string | |
| description | string | |
| workplaceType | one of: on-site, hybrid, remote, remote-first, flexible | |
| employmentType | one of: full-time, part-time, contractor, internship, volunteer | |
| roleWebsiteUrl | one of 2 shapes | |
| compensation | object | |
| locationNames | array of string | |
| categoryNames | array of string | |
| templateIds | array of string | Role template ids to seed criteria and questions from |
duplicate_roleDuplicate a role
Duplicate an existing role into a new draft, copying its details, settings, criteria and questions. Returns the new role's id.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| roleId* | string | The role to copy |
import_roleAdopt an imported role
Adopt a role synced from the ATS: moves it from 'imported' to a draft the team can configure and publish. Use list_roles with status 'imported' to find adoptable roles.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| roleId* | string |
update_role_detailsUpdate a role's details
Update a role's details. Provide only the fields to change; everything else keeps its current value. Location and category names replace the existing sets. The response lists any fields locked to a connected ATS which kept their synced values.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| roleId* | string | |
| name | string | |
| internalName | string | |
| shortDescription | string | |
| description | string | |
| workplaceType | one of: on-site, hybrid, remote, remote-first, flexible | |
| employmentType | one of: full-time, part-time, contractor, internship, volunteer | |
| roleWebsiteUrl | one of 2 shapes | |
| compensation | object | |
| locationNames | array of string | |
| categoryNames | array of string |
publish_rolePublish a role
Publish a role: opens it for applications, putting it live on the job board and apply page and approving any criteria still awaiting review. Also resumes a paused role and reopens a closed one, restarting progress towards any pause goal. Fails with the missing fields when the role's details are incomplete.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| roleId* | string |
pause_rolePause a role
Pause an open role: candidates can still apply, but new applications wait to be assessed until the role is resumed with publish_role. Fails when the role is not open.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| roleId* | string |
close_roleCHANGES DATAClose a role
Close an open or paused role: candidates can no longer apply. Reopen it with publish_role. Fails when the role is not open or paused.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| roleId* | string |
archive_roleCHANGES DATAArchive a role
Archive a role: hides it from active lists and stops new applications. Archived roles can be restored in First. Fails when the role is already archived.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| roleId* | string |
update_role_processCHANGES DATAUpdate a role's application process
Apply changes to a role's application process: its criteria and questions. Each change is one item of the shared process-change contract, applied in order. Criteria changed on a live role may await review and are approved when the role is next published. Use get_role first to see the current criteria and questions.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| roleId* | string | |
| changes* | array of one of 18 shapes | The process changes to apply, in order |
remove_role_process_componentCHANGES DATARemove a criterion or question from a role
Remove a criterion or question from a role's application process by process component type and subtype. For stage criteria (type 'assess-stage'), pass the specific criterionId — otherwise every stage criterion on the role would be removed.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| roleId* | string | |
| type* | one of: collect-basic-details, check-requirements, check-skills-and-experience, test-knowledge-and-behaviour, ask-video-question, survey, assess-stage, legacy-criterion, legacy-question | |
| itemType* | one of 7 shapes | |
| questionId | string | |
| criterionId | string | Required for stage criteria (type 'assess-stage'): without it, every stage criterion on the role would be removed |
Search the organisation's applications.
search_applicationsSearch applications
Search the organisation's applications by name or email across every role. Only returns applications for a role the token owner is allowed to see. Use this to find a specific person by name or email address. Omit `query` to list rather than search: `list: "hired"` with no query lists hires, which is how you answer "who have we hired recently" — there is no search term that returns them all. Each row carries `hiredAt`, `shortlistedAt` and `rejectedAt`, so filter to a date window on those rather than on `createdAt`, which is when the application arrived rather than when the outcome happened. **Read `truncated` before quoting any total.** When it is true this is a partial answer — `found` is a lower bound and paging to the end will not reach the rest, so say the count is at least that rather than stating it as the number. Only when `truncated` is false is `found` the whole answer.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| query | string | Name or email to search for. Omit to list the whole of `list` instead — there is no term that returns every hire. |
| field | one of: email, fullName | Restrict matching to one field; omit to match either |
| list | one of: inbox, shortlisted, rejected, hired, all | |
| page | integer |
get_applicationREAD ONLYGet an application
One application's record: which stage it has reached, when it was hired, shortlisted or rejected, where the candidate came from, and its assessment score with the criteria version behind it. Search finds the person; this says what happened to them. Returns not found for a role you are not allowed to see. Their work and education history comes back too, taken from the CV already held against the application — use this rather than the web-sourcing tools for anyone who has applied before: it costs nothing and it is their own CV, not a third-party guess at it. Pass includeProfile: false to leave it out.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| applicationId* | string | The application's id, as returned by search_applications. |
| includeProfile | boolean | Set false to leave out the candidate's CV history. Included by default. |
Source candidates from the organisation's own indexed candidate pool — for an existing role, or against a draft before any role exists — and from the public web, and add them as applications.
source_candidatesSource candidates for a role
Search the organisation's own indexed candidate pool for people matching a role's live criteria, and save a page of matches to the role's sourcing workspace. Choose the scope with searchType: 'radius' (near a location), 'country' (a whole country by ISO code) or 'everywhere'. Each call excludes candidates already saved for the role, so repeating it returns the next page. Add the returned candidates with add_sourced_candidates.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| roleId* | string | |
| searchType* | one of: radius, country, everywhere | The scope of the search: 'radius' (near a location), 'country' (a whole country by ISO code) or 'everywhere' (the whole organisation). |
| location | object | Only for searchType 'radius': the coordinates to source within a commute radius of. Ignored for 'country' and 'everywhere'. |
| radiusKm | number | Only for searchType 'radius': commute radius in km around the location. Defaults to 50km. |
| country | string | Only for searchType 'country': the country to restrict to, as an ISO 3166-1 alpha-2 code (e.g. 'GB'). Ignored for the other scopes. |
| limit | integer | How many new candidates to source, save and return — one page of results. Defaults to 4. Each search excludes everyone already saved for this role, so a follow-up search returns the next page. |
add_sourced_candidatesAdd sourced candidates to a role
Add previously sourced candidates to a role as applications. The contactIds are the ids returned by a source tool. For candidates sourced from a draft role, pass the same `reference` that search used. The response lists the ids that were added; ids that no longer resolve to a saved candidate are simply absent.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| roleId* | string | |
| contactIds* | array of string | The ids of the saved sourced candidates to add to the role as applications — the `id` values from a prior source-candidates response. |
| reference | string | The workspace the candidates were saved under. Pass the `reference` from a draft-role search; omit for candidates sourced directly for this role. |
source_candidates_from_draft_roleSource candidates from a draft role
Search the organisation's own indexed candidate pool against a draft role — no role required. Pass `criteriaChanges` from generate_suggested_criteria: that is what makes the search discriminate, and gathering a seniority and a few skills first is enough to generate them. A job description sharpens them further. A title on its own is a last resort — it matches anyone whose CV carries those words, so everyone scores strongly and the bands stop meaning anything. Omit `reference` on the first search and reuse the one returned for every later call — each search excludes candidates already saved to that workspace, so repeating it returns the next page. Choose the scope with searchType: 'radius', 'country' or 'everywhere'.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| title* | string | The draft role's job title. |
| jobDescription | string | The draft role's job description, once written. Sharpens the discipline match and pulls the role's own skills into the search. |
| keywords | array of string | Skills or keywords to aim the search at when there is no job description or criteria yet, e.g. ['React', 'Node.js']. The quickest but least precise input. |
| criteriaChanges | array of one of 18 shapes | The draft's agreed criteria — the `changes` from generating suggested criteria. The most accurate input: apply the same set to the role on creation and sourced candidates match on the requirements they are later assessed against. Only skills-and-experience changes feed the match. |
| reference | string | The sourcing workspace to save matches into. Omit on the first search and reuse the `reference` returned in the response for every later call, so follow-up searches page through results rather than repeating them. Pass it to the add endpoint to promote saved candidates into a role. |
| searchType* | one of: radius, country, everywhere | The scope of the search: 'radius' (near a location), 'country' (a whole country by ISO code) or 'everywhere' (the whole organisation). |
| location | object | Only for searchType 'radius': the coordinates to source within a commute radius of. Ignored for 'country' and 'everywhere'. |
| radiusKm | number | Only for searchType 'radius': commute radius in km around the location. Defaults to 50km. |
| country | string | Only for searchType 'country': the country to restrict to, as an ISO 3166-1 alpha-2 code (e.g. 'GB'). Ignored for the other scopes. |
| limit | integer | How many new candidates to source, save and return — one page of results. Defaults to 4. Each search excludes everyone already saved for this role, so a follow-up search returns the next page. |
get_sourced_candidatesList a sourcing workspace
List every candidate saved under a sourcing `reference`. Searches only return the page they sourced, so use this to see everything accumulated so far — after losing track of a page, or before deciding who to add to a role. An empty or unknown reference returns an empty list.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| reference* | string | The sourcing workspace to list. |
clear_sourced_candidatesCHANGES DATAEmpty a sourcing workspace
Remove every candidate saved under a sourcing `reference`. This also resets what searches exclude, so a later search on the same reference can return people it had already saved. Starting a fresh reference achieves the same without discarding anything, so prefer that unless the user asks to start this one over.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| reference* | string | The sourcing workspace to empty. |
search_web_for_candidatesREAD ONLYSearch the web for candidates
Search the PUBLIC WEB for candidate profiles with a Google X-Ray query (e.g. site:linkedin.com/in with OR-grouped job titles and must-have skills). This is the ONLY tool that finds people who have never applied to this organisation — every other search reads its own candidate database. It saves nothing: pass the profiles worth keeping to save_web_candidates. There is no location parameter — put the location into the query text.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| query* | string | A Google X-Ray query, e.g. site:linkedin.com/in with OR-grouped job titles and must-have skills. There is no location parameter — put the location scope into the query text. |
| num | integer | How many results to return, 1-10. Defaults to 3. |
| start | integer | The 1-based index of the first result, for paging. Defaults to 1; step by `num` to page. |
| dateRestrict | string | Restrict results by age: a unit letter followed by a number — "d7" (past 7 days), "w2" (past 2 weeks), "m6" (past 6 months), "y5" (past 5 years). The letter comes first. |
save_web_candidatesSave web-sourced candidates
Enrich up to 10 web-sourced profiles and save them to a sourcing workspace. This is the save step for web results — the database searches already save their own. Pass the `reference` from an earlier search to keep everything in one workspace, or omit it to start one and reuse the reference that comes back.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| contacts* | array of object | The profiles worth keeping, built from a prior web search. Enrichment is billed per contact, so send only the ones the caller has decided on. |
| reference | string | The workspace to save into. Pass the `reference` from an earlier search to keep everything together; omit to start a new one, whose reference comes back in the response. |
geocode_placeResolve a place to coordinates
Turn a city or place name into the coordinates a 'radius' search needs. Call this before sourcing near somewhere, then pass the coordinates as `location`. Returns found: false when the name is not an actual city (a whole country, or unrecognisable) — use geocode_country for a country, or fall back to searchType 'everywhere'.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| name* | string | The place to resolve, e.g. 'London' or 'Leeds, UK'. |
geocode_countryResolve a country to its ISO code
Turn a country a recruiter named — 'United Kingdom', 'UK' or 'GB' — into the ISO 3166-1 alpha-2 code a 'country' search needs. Call this before sourcing country-wide, then pass the code as `country`. Returns found: false when the value is not a recognisable country.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| name* | string | The country to resolve, e.g. 'United Kingdom'. |
Generate a role's assessment criteria from its title and job description.
generate_suggested_criteriaGenerate suggested role criteria
Generate a role's assessment criteria from its title and job description. Returns process changes ready to source against (pass them as `criteriaChanges` to source_candidates_from_draft_role) and to apply to a role (update_role_process) — using the same set for both is what makes sourced candidates match on the requirements they are later assessed against. Nothing is created or changed: show the criteria to the user, adjust if they want, then source. `requirementsPreview` shows what the matcher will score against. ALWAYS pass `roleId` when the role already exists — when you are working on a role rather than drafting one. That is what pulls in its intake call transcripts and note-taker summaries, which say what the hiring manager actually asked for; a job description is often boilerplate. Omitting it silently generates from the description alone. Omit it only while the role is still a draft.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| title* | string | The role title. |
| jobDescription | string | The role's job description. The richer this is, the better the criteria. |
| keywords | array of string | Skills the hiring manager called out, when there is no job description yet. |
| roleId | string | An existing role to draw intake material from. Its saved intake call transcripts and note-taker summaries are read in alongside the job description. Omit when the role does not exist yet. |
Research the talent market for a role — pool depth, pay and who else is hiring. The only tools here that reach the web.
research_talent_marketResearch a talent market
Research the talent market for a role — how deep the pool is, what it pays, and who else is hiring for it. Use it before committing to a hire, to sanity-check a salary, or to tell the user whether a location is realistic. Searches the web, not the organisation's candidate database (source_candidates_from_draft_role does that). Nothing is created or changed. `location` is a place as the recruiter would say it — 'London', 'the UK' — not coordinates or an ISO code. A market it finds nothing for returns found: false; say so rather than retrying.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | |
| roleTitle* | string | The role to research, e.g. 'Software Engineer'. |
| location* | string | Where to research it, as the recruiter would say it — 'London', 'the UK', 'Berlin'. Not a coordinate or ISO code. |
| seniorityLevel | string | The seniority to narrow to, e.g. 'senior', 'lead'. |
| additionalContext | string | Anything else that shapes the market, e.g. an industry or a required technology. |
Monthly usage reporting.
get_monthly_reportingGet monthly recruitment reporting
Monthly reporting for an organisation: one row per role with application volumes (in period, total, last 7/30 days, rolling daily averages), hires and time to hire (average/median days), completion funnel percentages (started, complete, shortlisted), stalled and aged application counts, role status and whether the role uses AI assessment. Defaults to the latest available month. Use this to compare roles; use get_reporting_summary for org-wide trends and get_stage_reporting for pipeline bottlenecks.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | The organisation to report on. |
| month | string | Report month in YYYY-MM format. Omit for the latest available month. |
get_stage_reportingGet pipeline stage reporting
Pipeline funnel reporting for an organisation: one row per role per pipeline stage, in pipeline order, with how many applications reached each stage and the average/median days spent in it. Use this to find where candidates drop off or wait the longest (pass-through rates, bottlenecks). Defaults to the latest available month. Months before stage metrics existed return no rows.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | The organisation to report on. |
| month | string | Report month in YYYY-MM format. Omit for the latest available month. |
get_reporting_summaryGet organisation reporting summary
Organisation-level trend reporting: one row per month with totals across every role — application volumes, hires and time to hire (average/median days), completion funnel percentages, aged applications and the share of roles using AI assessment. Use this first for topline questions and month-on-month trends. from/to are inclusive YYYY-MM bounds (at most 24 months); omit both for the trailing 12 months. Months without a report are omitted.
| Field | Type | Description |
|---|---|---|
| organisationId* | string | The organisation to report on. |
| from | string | First month in YYYY-MM format, inclusive. |
| to | string | Last month in YYYY-MM format, inclusive. |
The same operations are available over HTTP — see the REST API reference.