DEV Community

earnnova-dev
earnnova-dev

Posted on

I gave my AI agent live job-market data with a 3-tool MCP server (no API key, stdlib-only)

I build Remote Jobs API — a free, keyless endpoint that normalizes live remote-job listings across a dozen boards into one stable schema. The obvious next step was to give it to my AI agent. So I wrapped it in a Model Context Protocol server: remote-jobs-api-mcp.

Now any MCP client — Claude Desktop, Cursor, Claude Code, or a raw harness — can ask for live job data as a tool call. No API key, no account, no scraping, no stale snapshot baked into a prompt. The agent gets the current feed.

The problem with how agents get job data today

The usual options are all a bit sad:

  • Scrape a job board from the agent's prompt. Fragile, changes monthly, and your agent has to re-parse HTML every time.
  • Paste a snapshot into context. It's stale the moment you write it, and it burns tokens on 400-job dumps.
  • A keyed API. Fine for a human who returns tomorrow; wrong for an agent that needs one clean call and shouldn't hold a standing credential that can leak.

What an agent actually wants is: call a tool, get a lean, structured answer, done.

What the MCP server gives the agent

Three tools, all backed by the live keyless API:

  • search_jobs — live jobs by skill keywords and/or a minimum-salary floor. Returns a lean per-job shape (title, company, location, salary, category, source, link) — the long description is dropped so it doesn't pollute the context.
  • market_stats — live feed health: which boards are up and how many jobs are live right now.
  • salary_band — a live min / median / max salary band for a skill filter, so the agent can answer "what does a senior backend engineer pay, remote?" from real listings, not a cached number.

Here's the lean shape the search tool returns per job — deliberately trimmed for LLM consumption:

def _trim(job):
    return {
        "title": job.get("title"),
        "company": job.get("company"),
        "location": job.get("location"),
        "salary": job.get("salary"),
        "salary_min": job.get("salary_min"),
        "salary_max": job.get("salary_max"),
        "category": job.get("category"),
        "source": job.get("source"),
        "url": job.get("url"),
    }
Enter fullscreen mode Exit fullscreen mode

The salary band is computed live from the current feed — only jobs that actually disclose a numeric floor are counted, so you never get a "median" from nothing:

def salary_band(skills=None, limit=100):
    d = _get("/v1/jobs", params)
    floors = sorted(int(j["salary_min"]) for j in d.get("jobs", []) if j.get("salary_min"))
    if not floors:
        return {"n_with_salary": 0, "note": "no live jobs disclose a numeric salary for this filter"}
    return {"n_with_salary": len(floors), "min": floors[0],
            "median": floors[len(floors)//2], "max": floors[-1]}
Enter fullscreen mode Exit fullscreen mode

And the tool registration is just the official mcp SDK:

mcp = FastMCP("remote-jobs-api")

@mcp.tool()
def search_jobs_tool(skills: str = "", min_salary: int = 0, limit: int = 10) -> str:
    """Search live normalized remote jobs. skills = comma-separated; min_salary =
    floor on parsed annual salary. No API key required."""
    return json.dumps(search_jobs(skills or None, min_salary or None, limit))
Enter fullscreen mode Exit fullscreen mode

Install and wire it up

It's a thin, stdlib-only client (urllib) around the public API — the only dependency is the mcp SDK for the transport. Install from the repo:

git clone https://github.com/earnnova-dev/remote-jobs-api-mcp
cd remote-jobs-api-mcp && pip install .
Enter fullscreen mode Exit fullscreen mode

Then point your MCP client at it. For Claude Desktop / Claude Code:

{
  "mcpServers": {
    "remote-jobs-api": {
      "command": "remote-jobs-api-mcp",
      "env": { "RJA_BASE_URL": "https://remote-jobs-api.tten.no" }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

No env vars required — the base URL defaults to the public keyless instance. It also runs in Docker (docker run -i --rm remote-jobs-api-mcp over stdio).

What I like about this shape

  • The agent gets live data, not a snapshot. market_stats tells you how many jobs are live now — I checked, ~510,000 across 16 sources at the time of writing.
  • Graceful degradation. The pure helpers (search, stats, band) are importable and unit-testable without the MCP SDK. If the SDK isn't installed, the server tells you exactly what to pip install instead of crashing opaquely.
  • No credential in the loop. Nothing to leak, nothing to revoke, nothing standing. The agent pays nothing and signs nothing; it just calls a tool.
  • Stdlib client. The HTTP layer is urllib. If you can run python3, you can run this.

Honest caveats

  • It's the client only — a thin wrapper over a public API. No backend logic, no secrets, no data of its own. The value is the shape and the liveness, not a dataset.
  • Salary coverage depends on how many live listings disclose a numeric floor; the band is only as good as what the boards publish. I built it to report "no data" rather than invent a median.
  • It's not on PyPI yet — install from the repo for now.

What I'm curious about

  • What's the most useful shape of job data to hand an agent — lean like this, or do you want the full description in a separate "details" tool?
  • Do you gate a tool behind a check (agent confirms it's worth calling) before it hits the network, or let it fire freely?
  • For a salary-band tool, do you trust a single live sample, or would you want a rolling window so it's stable across the day?

It's MIT and open source: github.com/earnnova-dev/remote-jobs-api-mcp. Feedback welcome.

Top comments (0)