search_jobs
Search for jobs across Australian platforms (Seek, LinkedIn) using keywords, location, salary, employment type, and other filters.
Instructions
Search for jobs across Australian job platforms: Seek and LinkedIn. Supports keyword search, location with radius, employment type, salary, date posted, and application count filters.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| keyword | Yes | Job title, skill, or keyword to search for (e.g. "software engineer", "data analyst") | |
| location | Yes | Australian city or suburb (e.g. "Melbourne", "Sydney CBD", "Brisbane QLD", "Perth") | |
| radius | No | Search radius in km from the location (default: 50) | |
| employment_type | No | Filter by employment type. Multiple values allowed. | |
| salary_type | No | Salary payment frequency for filtering | |
| salary_min | No | Minimum salary (in the currency unit matching salary_type, e.g. 80000 for yearly AUD) | |
| salary_max | No | Maximum salary | |
| date_posted | No | Only show jobs posted within this time window | |
| max_applications | No | Only show jobs with fewer than this many applicants (e.g. 10, 20, 25). Not all platforms expose application count. | |
| platforms | No | Which platforms to search (default: all) | |
| page | No | Page number for pagination (default: 1) | |
| per_page | No | Results per page per platform (default: 10, max: 25) |
Implementation Reference
- src/index.ts:24-98 (registration)Registration of the 'search_jobs' tool in the ListTools request handler, defining its name, description, and input JSON schema.
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'search_jobs', description: 'Search for jobs across Australian job platforms: Seek and LinkedIn. Supports keyword search, location with radius, employment type, salary, date posted, and application count filters.', inputSchema: { type: 'object', required: ['keyword', 'location'], properties: { keyword: { type: 'string', description: 'Job title, skill, or keyword to search for (e.g. "software engineer", "data analyst")', }, location: { type: 'string', description: 'Australian city or suburb (e.g. "Melbourne", "Sydney CBD", "Brisbane QLD", "Perth")', }, radius: { type: 'number', description: 'Search radius in km from the location (default: 50)', default: 50, minimum: 5, maximum: 200, }, employment_type: { type: 'array', description: 'Filter by employment type. Multiple values allowed.', items: { type: 'string', enum: ['fulltime', 'parttime', 'contract', 'casual'] }, }, salary_type: { type: 'string', description: 'Salary payment frequency for filtering', enum: ['hourly', 'monthly', 'yearly'], }, salary_min: { type: 'number', description: 'Minimum salary (in the currency unit matching salary_type, e.g. 80000 for yearly AUD)', }, salary_max: { type: 'number', description: 'Maximum salary', }, date_posted: { type: 'string', description: 'Only show jobs posted within this time window', enum: ['24h', '2d', '3d', '7d', '30d'], }, max_applications: { type: 'number', description: 'Only show jobs with fewer than this many applicants (e.g. 10, 20, 25). Not all platforms expose application count.', }, platforms: { type: 'array', description: 'Which platforms to search (default: all)', items: { type: 'string', enum: ['seek', 'linkedin'] }, }, page: { type: 'number', description: 'Page number for pagination (default: 1)', default: 1, minimum: 1, }, per_page: { type: 'number', description: 'Results per page per platform (default: 10, max: 25)', default: 10, minimum: 1, maximum: 25, }, }, }, }, ], })); - src/index.ts:100-160 (handler)Handler for 'search_jobs' tool call. Parses arguments into JobSearchParams, dispatches to selected platform scrapers (seek/linkedin) via Promise.allSettled, and returns aggregated results.
server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name !== 'search_jobs') { return { content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }], isError: true }; } const args = request.params.arguments as Record<string, unknown>; const params: JobSearchParams = { keyword: String(args.keyword ?? ''), location: String(args.location ?? ''), radius: typeof args.radius === 'number' ? args.radius : 50, employment_type: Array.isArray(args.employment_type) ? args.employment_type as JobSearchParams['employment_type'] : undefined, salary_type: typeof args.salary_type === 'string' ? args.salary_type as JobSearchParams['salary_type'] : undefined, salary_min: typeof args.salary_min === 'number' ? args.salary_min : undefined, salary_max: typeof args.salary_max === 'number' ? args.salary_max : undefined, date_posted: typeof args.date_posted === 'string' ? args.date_posted as JobSearchParams['date_posted'] : undefined, max_applications: typeof args.max_applications === 'number' ? args.max_applications : undefined, platforms: Array.isArray(args.platforms) ? args.platforms as Platform[] : ALL_PLATFORMS, page: typeof args.page === 'number' ? args.page : 1, per_page: typeof args.per_page === 'number' ? Math.min(args.per_page, 25) : 10, }; if (!params.keyword) { return { content: [{ type: 'text', text: 'keyword is required' }], isError: true }; } if (!params.location) { return { content: [{ type: 'text', text: 'location is required' }], isError: true }; } const targetPlatforms = params.platforms ?? ALL_PLATFORMS; const settled = await Promise.allSettled( targetPlatforms.map((p) => scraperMap[p](params)) ); const results = settled.map((s, i) => { if (s.status === 'fulfilled') return s.value; return { platform: targetPlatforms[i], jobs: [], page: params.page ?? 1, error: s.reason instanceof Error ? s.reason.message : String(s.reason), }; }); const response = { results, params, fetched_at: new Date().toISOString(), summary: results.map((r) => ({ platform: r.platform, count: r.jobs.length, total: r.total, error: r.error, })), }; return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }], }; }); - src/types.ts:6-19 (schema)Type definition for JobSearchParams used as input to the search_jobs tool.
export interface JobSearchParams { keyword: string; location: string; radius?: number; // default 50 (km) employment_type?: EmploymentType[]; salary_type?: SalaryType; salary_min?: number; salary_max?: number; date_posted?: DatePosted; max_applications?: number; platforms?: Platform[]; page?: number; per_page?: number; // default 10, max 25 } - src/scrapers/seek.ts:59-164 (helper)Seek platform scraper implementation - called by the handler to scrape jobs from au.seek.com using Apollo SSR cache parsing.
export async function seekSearch(params: JobSearchParams): Promise<PlatformResult> { // Seek migrated from www.seek.com.au to au.seek.com. // The old /api/chalice-search endpoint is gone (404). Instead we hit the // search results page and parse the Apollo SSR cache embedded in the HTML. const client = createHttpClient({ Referer: 'https://au.seek.com/', 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate', 'Sec-Fetch-Site': 'none', 'Sec-Fetch-User': '?1', 'Upgrade-Insecure-Requests': '1', }); const page = params.page ?? 1; const keywordSlug = slugify(params.keyword.toLowerCase()); const locationSlug = slugify(params.location); const url = `https://au.seek.com/${keywordSlug}-jobs/in-${locationSlug}`; const query: Record<string, string | number> = { page }; if (params.date_posted) query.daterange = DATE_RANGE_MAP[params.date_posted]; if (params.employment_type?.length) { query.worktype = params.employment_type.map((t) => WORK_TYPE_MAP[t]).join(','); } if (params.salary_type) query.salarytype = SALARY_TYPE_MAP[params.salary_type]; if (params.salary_min != null || params.salary_max != null) { const min = params.salary_min ?? 0; const max = params.salary_max ?? 999999; query.salaryrange = `${min}-${max}`; } try { const resp = await client.get(url, { params: query, headers: { Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'en-AU,en;q=0.9', }, }); const html = resp.data as string; const cache = extractApolloData(html); if (!cache) { return { platform: 'seek', jobs: [], page, error: 'Seek: could not extract Apollo cache from page' }; } // Find the jobSearchV6(...) key in ROOT_QUERY const rootQuery = cache['ROOT_QUERY'] as Record<string, unknown> | undefined; if (!rootQuery) { return { platform: 'seek', jobs: [], page, error: 'Seek: ROOT_QUERY not found in Apollo cache' }; } const jobSearchKey = Object.keys(rootQuery).find((k) => k.startsWith('jobSearchV6(')); if (!jobSearchKey) { return { platform: 'seek', jobs: [], page, error: 'Seek: no jobSearchV6 result in Apollo cache' }; } const result = rootQuery[jobSearchKey] as { data?: Array<{ __ref?: string }>; totalCount?: number }; const refs = result?.data ?? []; const perPage = Math.min(params.per_page ?? 10, 25); const jobs: Job[] = []; for (const ref of refs) { if (!ref.__ref) continue; const jobData = resolveRef(cache, ref.__ref); if (!jobData) continue; const appCount = null; // Not exposed in Apollo cache (requires separate job detail call) if (params.max_applications != null && appCount != null && (appCount as number) > params.max_applications) { continue; } const id = String(jobData.id ?? ''); const location = jobData.locations?.[0]?.label ?? ''; const postedAt = jobData.listingDate?.dateTimeUtc ?? ''; jobs.push({ id: makeJobId('seek', id), title: String(jobData.title ?? ''), company: String(jobData.companyName ?? ''), location, employment_type: undefined, salary: String(jobData.salaryLabel ?? '') || undefined, description: jobData.teaser ? truncate(String(jobData.teaser)) : undefined, url: `https://au.seek.com/job/${id}`, posted_at: postedAt || undefined, applications_count: appCount, platform: 'seek', }); if (jobs.length >= perPage) break; } // totalCount lives in the GraphQL result but the Apollo cache stores it // on the SearchV6Result under `totalCount` if present (it's in the page state JSON) const totalCountMatch = html.match(/"totalCount":(\d+)/); const total = totalCountMatch ? parseInt(totalCountMatch[1], 10) : undefined; return { platform: 'seek', jobs, total, page }; } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); return { platform: 'seek', jobs: [], page, error: `Seek: ${msg}` }; } } - src/scrapers/linkedin.ts:26-106 (helper)LinkedIn platform scraper implementation - called by the handler to scrape jobs from LinkedIn's guest job search API using cheerio HTML parsing.
export async function linkedinSearch(params: JobSearchParams): Promise<PlatformResult> { const client = createHttpClient({ Referer: 'https://www.linkedin.com/', 'X-Li-Lang': 'en_AU', }); const page = params.page ?? 1; const perPage = Math.min(params.per_page ?? 10, 25); const start = (page - 1) * perPage; const radiusMiles = kmToMiles(params.radius ?? 50); const query: Record<string, string | number> = { keywords: params.keyword, location: `${params.location}, Australia`, distance: radiusMiles, start, count: perPage, }; if (params.employment_type?.length) { query.f_JT = params.employment_type.map((t) => JOB_TYPE_MAP[t]).join(','); } if (params.date_posted) { query.f_TPR = TIME_FILTER_MAP[params.date_posted]; } try { const resp = await client.get( 'https://www.linkedin.com/jobs-guest/jobs/api/seeMoreJobPostings/search', { params: query, headers: { Accept: 'text/html,*/*' } } ); const $ = cheerio.load(resp.data as string); const jobs: Job[] = []; $('li').each((_, el) => { const card = $(el); const linkEl = card.find('a.base-card__full-link'); const url = linkEl.attr('href')?.split('?')[0] ?? ''; const title = card.find('.base-search-card__title').text().trim() || linkEl.attr('aria-label')?.trim() || ''; const company = card.find('.base-search-card__subtitle').text().trim(); const location = card.find('.job-search-card__location').text().trim(); const postedAt = card.find('time').attr('datetime') || card.find('.job-search-card__listdate').text().trim(); const applicantText = card .find('.num-applicants__caption, .applicant-count') .text() .trim(); const appCount = applicantText ? parseApplicationCount(applicantText) : null; if (!title || !url) return; if (params.max_applications != null && appCount != null && appCount > params.max_applications) { return; } const rawId = url.split('/').filter(Boolean).pop() ?? url; jobs.push({ id: makeJobId('linkedin', rawId), title, company, location, employment_type: undefined, salary: undefined, description: undefined, url, posted_at: postedAt || undefined, applications_count: appCount, platform: 'linkedin', }); }); return { platform: 'linkedin', jobs, page }; } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); return { platform: 'linkedin', jobs: [], page, error: msg }; } }