Skip to main content
Glama

search_jobs

Find current job listings with freshness indicators to avoid outdated postings. Filter by location, remote work, keywords, and recency across multiple job boards.

Instructions

Search for real-time job listings with freshness badges on every result — so you never apply to a role that closed months ago. Sources: Remotive + RemoteOK + The Muse + HN 'Who is Hiring'. Supports location filtering, remote-only mode, keyword spotting (e.g. FIFO), and max age filtering. Returns timestamped freshcontext sorted freshest first.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
queryYesJob search query e.g. 'typescript', 'mining engineer', 'FIFO operator', 'data analyst'
locationNoCountry, city, or 'remote' / 'worldwide' e.g. 'South Africa', 'Australia', 'remote'
remote_onlyNoOnly return remote-friendly listings
max_age_daysNoHide listings older than N days (default 60, use 7 for very fresh only)
keywordsNoKeywords to highlight in results e.g. ['FIFO', 'underground', 'contract']
max_lengthNo

Implementation Reference

  • Registration of the search_jobs tool and its handler in src/server.ts.
    server.registerTool(
      "search_jobs",
      {
        description:
          "Search for real-time job listings with freshness badges on every result — so you never apply to a role that closed months ago. Sources: Remotive + RemoteOK + The Muse + HN 'Who is Hiring'. Supports location filtering, remote-only mode, keyword spotting (e.g. FIFO), and max age filtering. Returns timestamped freshcontext sorted freshest first.",
        inputSchema: z.object({
          query: z.string().describe("Job search query e.g. 'typescript', 'mining engineer', 'FIFO operator', 'data analyst'"),
          location: z.string().optional().describe("Country, city, or 'remote' / 'worldwide' e.g. 'South Africa', 'Australia', 'remote'"),
          remote_only: z.boolean().optional().default(false).describe("Only return remote-friendly listings"),
          max_age_days: z.number().optional().default(60).describe("Hide listings older than N days (default 60, use 7 for very fresh only)"),
          keywords: z.array(z.string()).optional().default([]).describe("Keywords to highlight in results e.g. ['FIFO', 'underground', 'contract']"),
          max_length: z.number().optional().default(8000),
        }),
        annotations: { readOnlyHint: true, openWorldHint: true },
      },
      async ({ query, location, remote_only, max_age_days, keywords, max_length }) => {
        try {
          const result = await jobsAdapter({
            url: query,
            maxLength: max_length,
            location: location ?? "",
            remoteOnly: remote_only,
            maxAgeDays: max_age_days,
            keywords: keywords ?? [],
          });
          const ctx = stampFreshness(result, { url: query, maxLength: max_length }, "jobs");
          return { content: [{ type: "text", text: formatForLLM(ctx) }] };
        } catch (err) {
          return { content: [{ type: "text", text: formatSecurityError(err) }] };
        }
      }
    );
  • The jobsAdapter implementation that orchestrates fetching jobs from multiple sources.
    export async function jobsAdapter(options: ExtractOptions): Promise<AdapterResult> {
      const query      = options.url.trim();
      const maxLength  = options.maxLength ?? 8000;
      const location   = options.location ?? "";
      const remoteOnly = options.remoteOnly ?? false;
      const maxAgeDays = options.maxAgeDays ?? 60;
      const keywords   = options.keywords ?? [];
    
      const [remotiveRes, remoteOkRes, arbeitnowRes, museRes, hnRes] = await Promise.allSettled([
        fetchRemotive(query, location, maxAgeDays, keywords),
        fetchRemoteOK(query, location, maxAgeDays, keywords),
        fetchArbeitnow(query, location, maxAgeDays, keywords, remoteOnly),
        remoteOnly ? Promise.reject("skipped") : fetchMuse(query, location, maxAgeDays, keywords),
        fetchHNHiring(query, location, maxAgeDays, keywords),
      ]);
    
      const pool: Listing[] = [];
      const sourceStats: Record<string, number> = {};
    
      const harvest = (res: PromiseSettledResult<{ listings: Listing[] }>, label: string) => {
        if (res.status === "fulfilled") {
          pool.push(...res.value.listings);
          sourceStats[label] = res.value.listings.length;
        } else {
          sourceStats[label] = 0;
        }
      };
    
      harvest(remotiveRes as PromiseSettledResult<{ listings: Listing[] }>, "Remotive");
      harvest(remoteOkRes as PromiseSettledResult<{ listings: Listing[] }>, "RemoteOK");
      harvest(arbeitnowRes as PromiseSettledResult<{ listings: Listing[] }>, "Arbeitnow");
      harvest(museRes as PromiseSettledResult<{ listings: Listing[] }>, "The Muse");
      harvest(hnRes as PromiseSettledResult<{ listings: Listing[] }>, "HN Hiring");
    
      if (!pool.length) {
        return {
          raw: [
            `No job listings found for "${query}"${location ? ` in ${location}` : ""}.`,
            "",
            "Tips:",
            "• Try broader terms e.g. \"engineer\" instead of \"senior TypeScript engineer\"",
            "• Set location to \"remote\" for worldwide results",
            "• Increase max_age_days (default: 60)",
            "• Note: FIFO/mining/trades jobs are on specialist boards (myJobsNamibia, SEEK, mining-specific sites) — these sources are tech/remote focused",
          ].join("\n"),
          content_date: null,
          freshness_confidence: "low",
        };
      }
    
      // Sort: freshest first
      pool.sort((a, b) => a.days - b.days);
    
      const freshCount = pool.filter(l => l.days <= 7).length;
      const goodCount  = pool.filter(l => l.days > 7 && l.days <= 30).length;
      const staleCount = pool.filter(l => l.days > 30).length;
    
      const sourceSummary = Object.entries(sourceStats)
        .map(([src, n]) => `${src}:${n}`)
        .join("  ");
    
      const header = [
        `# Job Search: "${query}"${location ? ` · ${location}` : ""}${remoteOnly ? " · remote only" : ""}`,
        `Retrieved: ${new Date().toISOString()}`,
        `Found: ${pool.length} listings — 🟢 ${freshCount} fresh  🟡 ${goodCount} recent  🔴 ${staleCount} older`,
        `Sources: ${sourceSummary}`,
        `⚠️  Sorted freshest first. Check badge before applying.`,
        keywords.length ? `🔍 Watching for: ${keywords.map(k => `⚡${k}`).join(", ")}` : null,
        "",
      ].filter(Boolean).join("\n");
    
      const body = pool
        .map(l => l.text)
        .join("\n\n─────────────────────────────\n\n");
    
      const raw = (header + "\n\n" + body).slice(0, maxLength);
    
      const freshestDays = pool[0]?.days ?? 9999;
      const newestDate = freshestDays < 9999
        ? new Date(Date.now() - freshestDays * 86400000).toISOString().slice(0, 10)
        : null;
    
      return {
        raw,
        content_date: newestDate,
        freshness_confidence: freshestDays <= 7 ? "high" : freshestDays <= 30 ? "medium" : "low",
      };
    }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/PrinceGabriel-lgtm/freshcontext-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server