Skip to main content
Glama

search_jobs

Read-only

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",
      };
    }
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations indicate read-only and open-world hints, which the description doesn't contradict. The description adds valuable behavioral context beyond annotations: it mentions 'freshness badges on every result,' timestamped results sorted freshest first, and that it prevents applying to closed roles. This provides insight into output behavior and user benefits not covered by annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with the core purpose and key feature (freshness badges), followed by supporting details in a single, efficient sentence. Every part adds value: sources, filtering capabilities, and result sorting without redundancy or fluff.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (6 parameters, no output schema), the description is fairly complete. It covers purpose, sources, key features, and result behavior. However, it could be more explicit about error handling or rate limits, though annotations provide some safety context with readOnlyHint and openWorldHint.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is high at 83%, so the baseline is 3. The description adds some semantic context by mentioning 'location filtering, remote-only mode, keyword spotting (e.g. FIFO), and max age filtering,' which aligns with parameters but doesn't provide additional details beyond what the schema already describes in its property descriptions.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool searches for 'real-time job listings with freshness badges' from specific sources (Remotive, RemoteOK, The Muse, HN 'Who is Hiring'), distinguishing it from sibling tools that extract data from other domains like GitHub, SEC filings, or HackerNews. It specifies the verb 'search' and resource 'job listings' with unique freshness features.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear context for when to use this tool by listing its supported features (location filtering, remote-only mode, keyword spotting, max age filtering) and sources. However, it doesn't explicitly state when NOT to use it or name specific alternatives among sibling tools, though the context implies it's for job searches versus other data extraction tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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