Remote Radar MCP
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Remote Radar MCPFind me remote React jobs in Europe posted this week"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Remote Radar MCP
An MCP server that helps you find remote developer jobs from official public job APIs, and keeps track of the ones you care about in a local Postgres database. Use it from Claude Desktop, Claude Code or any MCP client.
It finds, filters, dedupes, saves and tracks. It never applies to jobs. You read the listing and apply yourself, on the original site.
🔎
search_jobs: one search across Remotive and Arbeitnow, normalized into a single job shape, with duplicates removed and filters for keywords, tags, remote region and posting date.📌
save_job,update_job_status,list_saved_jobs: a small application tracker (found → applied → interview → offer, orrejected/skipped) with a dated notes log and a full status history.📄
get_job_details: the full listing as clean text, plus its tracker status.🤝 Respects the sources: official APIs only, rate limits enforced across restarts, source credit on every result.
Built with TypeScript (strict), the official MCP TypeScript SDK, zod, PostgreSQL with plain SQL migrations, and Vitest.
Demo
An example conversation in Claude Desktop. The companies and listings are made up; real results depend on what the sources have that day.
You: Find remote frontend or full-stack jobs I can do from Europe, posted in the last two weeks.
Claude: calls
search_jobswith{ "query": "react", "remote_scope": ["Worldwide", "Europe", "EU only"], "posted_within_days": 14 }I found 6 matches. The most relevant:
Senior Full-Stack Engineer (React/Node), Acme Cloud. Worldwide, full-time, $90k–$120k. via Remotive
Frontend Lead (TypeScript), Globex GmbH. Remote in Europe (Germany), full-time. via Arbeitnow
…
You: Tell me more about the Globex one. Do they require German?
Claude: calls
get_job_detailswith{ "job_id": "arbeitnow:frontend-lead-typescript-berlin-12345" }The listing is in English and asks for "fluent English; German is a plus", so German isn't required. They want 6+ years with React and TypeScript, experience leading a small team, and CI/CD. Remote within Europe, with optional visits to the Berlin office.
You: Save it. Note: strong stack match, ask about team size.
Claude: calls
save_job. Saved with status found.You: I applied through their careers page today.
Claude: calls
update_job_statuswith{ "status": "applied", "notes": "Applied via careers page" }Updated: found → applied.You: What's in my pipeline?
Claude: calls
list_saved_jobs. You have 3 saved jobs: 1 applied (Globex, today), 1 interview (Initech, since Oct 14), 1 found (Acme Cloud).
Related MCP server: Job Copilot MCP Server
Tools
Tool | Input | What it does |
|
| Fetches from all sources (cached), normalizes, dedupes, filters, and returns up to |
|
| The full normalized job, description converted to plain text, plus tracker status if saved. |
|
| Saves the job with status |
|
|
|
|
| Saved jobs with status and notes, most recently updated first. |
Search filters
query: every word must appear as a whole word (case- and accent-insensitive) in the title, company, tags, location or description.javadoesn't match "JavaScript";node.jsandc++work.tags: the job has at least one of these tags.remote_scope: one value or a list. Values:Worldwide,Europe,EU only,US only,Americas,Multi-region,Other,Unknown,On-site. AMulti-regionjob (e.g. "Europe, USA, APAC") matchesEuropeif Europe is in its list. Without this filter, on-site jobs are excluded.posted_within_days: 1–365.
Each tool returns structured output (with a declared output schema) plus a JSON text copy for
clients that don't support structured output yet. Read-only tools are annotated readOnlyHint;
only save_job and update_job_status write, and they only write to your local database.
The normalized job shape
{
id: "remotive:2091158", // "<source>:<source id>"
source: "remotive" | "arbeitnow",
url: string, // original listing: apply here
title: string,
company: string,
location: string | null, // raw text from the source
remote_scope: RemoteScope, // see above
employment_type: "full_time" | "part_time" | "contract" | "freelance" | "internship" | "unknown",
salary: string | null, // raw text, not parsed
tags: string[],
description: string, // HTML (plain text in get_job_details)
published_at: string // ISO 8601, UTC
}Setup
Requirements: Node.js 22.12+ and Docker (for Postgres).
git clone https://github.com/DarioKnezovic/remote-radar-mcp.git
cd remote-radar-mcp
npm install
npm run db:up # Postgres 18 on localhost:5433
npm run buildThe server runs database migrations automatically when it starts. You can also run them yourself
with npm run db:migrate.
Docker Desktop on macOS: if
dockersays it can't connect to/var/run/docker.sock, rundocker context use desktop-linuxonce.
Connect to Claude Code
claude mcp add remote-radar -- node /absolute/path/to/remote-radar-mcp/dist/index.js
claude mcp list # remote-radar should show as connectedConnect to Claude Desktop
Add this to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or
%APPDATA%\Claude\claude_desktop_config.json (Windows), then fully quit and reopen Claude Desktop:
{
"mcpServers": {
"remote-radar": {
"command": "node",
"args": ["/absolute/path/to/remote-radar-mcp/dist/index.js"],
"env": {
"DATABASE_URL": "postgres://remote_radar:remote_radar@localhost:5433/remote_radar"
}
}
}
}Postgres must be running before the client starts the server. If it isn't, the server exits with
"Is Postgres running? Try docker compose up -d", which appears in the client's MCP logs.
Try it without an AI client
npm run inspect # opens the MCP Inspector in your browserConfiguration
Variable | Default | Description |
|
| Matches |
|
|
|
Architecture
flowchart LR
client["Claude Desktop / Claude Code<br/>(MCP client)"]
subgraph server["remote-radar MCP server (stdio)"]
direction LR
tools["tools/<br/>search_jobs · get_job_details<br/>save_job · update_job_status · list_saved_jobs"]
search["JobSearchService<br/>merge → dedupe → filter → sort → limit"]
domain["domain/<br/>Job · remote scope · filter · dedupe"]
cache["CachedJobSource<br/>TTL · stale-on-error · retry throttle"]
remotive["RemotiveSource"]
arbeitnow["ArbeitnowSource"]
repo["JobRepository<br/>(all SQL)"]
end
db[("PostgreSQL<br/>jobs · saved_jobs<br/>job_status_history · source_fetches")]
rapi["remotive.com/api"]
aapi["arbeitnow.com/api"]
client <-->|JSON-RPC over stdio| tools
tools --> search
tools --> repo
search --> domain
search --> cache
cache --> remotive & arbeitnow
cache -->|FetchCacheStore| repo
remotive -->|HTTPS, ≤ 4/day| rapi
arbeitnow -->|HTTPS, ≤ 3 pages/hour| aapi
repo --> dbOne search, step by step
search_jobsasksJobSearchServicefor results.Each source is wrapped in
CachedJobSource. If the last successful fetch, recorded in Postgres, is still fresh (Remotive: 6 h, Arbeitnow: 1 h), the cached jobs are read from Postgres. Otherwise the source is called once, its jobs are upserted, and the fetch is recorded.The source client validates the raw response with zod, one listing at a time, so one bad listing doesn't drop the whole feed, and normalizes it into a
Job.Jobs from all sources are merged, deduped, filtered, sorted and cut to
limit.A failing source shows up as
ok: falsein the result, and the other sources' jobs are still returned. The tool only reports an error if every source fails.
Project layout
src/
index.ts entry point: config, migrations, wiring, stdio transport
server.ts createServer(deps): registers all tools (used directly by tests)
config.ts env vars validated with zod
domain/ pure logic, no I/O: Job, remote scope, filter, dedupe, text, application status
sources/ JobSource interface, HTTP client, cache, registry
remotive/ schema (raw API) · normalize · client
arbeitnow/ schema (raw API) · normalize · client (pagination)
services/ JobSearchService
db/ pool, migration runner, JobRepository, Postgres cache store
tools/ one file per MCP tool + shared output schemas
migrations/ plain SQL, applied in order
test/
fixtures/ recorded API responses (trimmed, see test/fixtures/README.md)
unit/ no network, no database
integration/ real Postgres, each file in its own temporary schemaDevelopment
Command | What it does |
| Run from source with tsx |
| Compile to |
| Unit tests (integration tests skip without a database) |
| All tests, including Postgres integration tests |
| All tests with a coverage report ( |
|
|
| ESLint (typescript-eslint strict, type-checked) |
| Prettier |
| Start / stop Postgres |
| Build and open the MCP Inspector |
Tests never call the real APIs. Normalization and dedupe are tested against responses
recorded from Remotive and Arbeitnow (test/fixtures/). The tool tests connect a real MCP client
to the server over an in-memory transport. Integration tests run the real migrations in a
throwaway Postgres schema.
Adding a job source
Create
src/sources/<name>/withschema.ts(zod schema of the raw API),normalize.ts(raw →Job) andclient.ts(a class implementingJobSource).Add the id to
SOURCE_IDSinsrc/domain/job.tsand an attribution line toSOURCE_ATTRIBUTIONinsrc/tools/shared.ts.Register it in
src/sources/registry.tswith a TTL that fits the source's rate limits.Record a sample response into
test/fixtures/(trimmed) and test the normalizer against it.
Nothing else changes: search, dedupe, caching and the tracker work on the normalized Job.
Design decisions
Why it never applies to jobs. Applying is the step where your judgement matters most and mistakes are hardest to undo: a generic auto-filled application can burn a good opportunity. Many job boards also forbid automated applications. So the server's job is to find, filter and remember. You read the listing and apply yourself. The tool descriptions and server instructions say this explicitly, so the model doesn't offer to "apply for you" either.
Why only official APIs. Scraping sites that forbid it (LinkedIn, Wellfound and others) breaks their terms and breaks whenever their HTML changes. Remotive and Arbeitnow both publish free APIs. Their terms shape the code:
Rate limits: Remotive advises at most 4 requests a day. Arbeitnow asks not to abuse its free API.
Attribution: every result carries the source name and the original URL.
No republishing: the test fixtures are trimmed samples rather than full copies of listings, and this README uses made-up listings.
Why a JobSource interface, with caching as a separate wrapper. Sources are messy in
different ways. Remotive gives free-text locations. Arbeitnow mixes languages in job_types,
sometimes reports remote jobs as remote: false, and sometimes puts the location in the title.
Each source keeps that mess in its own folder and outputs the same Job, so dedupe, filters
and the tracker are written once. Rate limiting is a separate concern, so CachedJobSource wraps
any source instead of each client caching on its own. Adding Greenhouse, Lever or Ashby is a new
folder and one line in the registry.
Why the cache lives in Postgres. MCP clients start a new server process for many sessions.
An in-memory cache would refetch on every start, which would quickly break Remotive's 4-per-day
limit. Fetches are recorded in source_fetches, and the jobs of the latest fetch are the rows
with last_seen_at at or after that fetch. This also means get_job_details works for any job
you've seen in a search, not only saved ones.
Why filtering happens locally. Remotive's documented search and limit parameters are
currently ignored by the free API, which always returns the latest 10 jobs. Arbeitnow has no
search parameters. Fetching the feed once and filtering locally behaves the same for every
source and costs nothing extra in requests.
Why remote scope is conservative. Location text is free-form. The classifier recognizes
regions (Worldwide, Europe, EU, US, Americas, APAC and others) and only picks a scope when the
text actually names one. "Remote" alone or a city name ("Berlin") stays Unknown rather than
guessed as Europe, because remote with a Berlin office often means remote within Germany only.
Region names in a title are used when the location has none ("…Remote from EU"), but "Global" or
"International" in a title is ignored ("Global Head of Sales").
Dedupe rule and its trade-off. Two listings are duplicates when the company matches after
cleanup (Acme GmbH = Acme, Inc.) and the titles' words overlap by at least 80%. Gender
markers like (m/w/d), "Remote", and spelling variants like front-end/frontend are ignored, but
seniority words are kept: "Senior Frontend Engineer" is a different role than "Frontend
Engineer". The copy with more information (salary, known scope, tags) is kept. Trade-off: the
same role posted for two cities (Berlin and Karlsruhe) collapses into one result, and the second
city isn't shown.
Why notes are a log. Notes are appended, not overwritten
([2026-10-21 applied] Sent CV via careers page), so a note added when saving ("referral from a
former colleague") survives later status changes. job_status_history keeps every transition.
Why plain SQL instead of an ORM. The schema is four tables. Hand-written SQL in a single
repository file is easy to read, uses Postgres features directly (bulk upsert via
jsonb_to_recordset, CHECK constraints, advisory locks for migrations), and has no build or
runtime magic. Migrations are numbered .sql files, each applied in a transaction.
Why stdout is off limits. With the stdio transport, stdout is the protocol channel, and one
stray console.log corrupts it. All logging goes to stderr as JSON lines, and ESLint rejects
console.log.
Sources
Source | Endpoint | Notes |
| Free API returns the latest ~10 jobs, delayed 24 h. Cached 6 h (≤ 4 requests/day). Credit Remotive and link to the listing. | |
| 300 jobs/page, mostly Germany/EU, mostly on-site. First 3 pages, cached 1 h. Personal, non-commercial use; link back. |
Job data belongs to these sources and the companies that posted it. This project is for personal job searching.
Limitations
Remotive's free API gives few jobs (about 10), so most results come from Arbeitnow, which is mostly German and on-site.
Most Arbeitnow remote jobs end up with scope
Unknown. IncludeUnknowninremote_scopeto see them.Salaries are raw text (
"$50-$75 /hour","OTE $25k - $35k"), and Arbeitnow has none, so there's no salary filter yet.
Roadmap (v2)
More sources: RemoteOK, and company boards on Greenhouse (
boards-api.greenhouse.io/v1/boards/{company}/jobs), Lever (api.lever.co/v0/postings/{company}?mode=json) and Ashby (api.ashbyhq.com/posting-api/job-board/{name}).get_profile: reads a local, git-ignoredprofile.json(skills, experience, location, citizenship, stories).match_job { job_id }: compares a job with the profile and returns matched skills, missing skills and red flags. Examples: "requires German", "on-site", "US only", "part-time", "requires EU residence while you live outside the EU". This will be a pure function insrc/domain/match.tsover the normalized job, which already keeps the full description for it.
License
MIT © 2026 Dario Knezović. The license covers this code, not the job listings it fetches, which belong to their sources.
This server cannot be deployed
Maintenance
Related MCP Connectors
Search remote tech jobs, inspect descriptions, compare roles, and retrieve application links.
Search remote jobs from company career pages by title, location, seniority, salary, and tech stack.
Unified jobs search over official feeds + ATS boards (USAJOBS, Adzuna, Muse, Greenhouse, Lever...)
Search remote jobs, compare salaries, create alerts, and request user-confirmed apply links.
Related MCP Servers
- FlicenseNot gradedqualityBmaintenanceEnables automated job searching from multiple boards, filtering matches against saved criteria, deduplicating seen postings, and tracking application statuses. Runs locally and integrates with Claude Desktop for natural-language queries and updates.-
- AlicenseNot gradedqualityBmaintenanceExposes the entire job-search workflow as 13 chat-driven tools, letting users refresh a ranked feed of live postings from employers' public hiring APIs, view and score job descriptions, tailor and render truthful one-page LaTeX resumes, review skill gaps and screening answers, and log applications. It keeps a human in the loop, since nothing is ever submitted automatically.MIT
- FlicenseNot gradedqualityAmaintenanceEnables AI agents and terminal users to search, filter by salary, remote work, and tech stack, and apply for roughly 4,700 salary-transparent tech jobs across nine job boards in eight countries, including Swiss and Singapore government portals. It keeps a local record of submitted applications so duplicates are surfaced as data rather than errors.-
- FlicenseNot gradedqualityAmaintenanceEnables an AI agent to search, filter by salary, remote work, tech and country, and apply to roughly 4,700 salary-transparent tech postings across nine boards and eight countries, and to list which boards are available and what each one supports. It also tracks applications locally so already-applied roles disappear from results.-