Skip to main content
Glama
salman0butt

Remote Job Agent MCP

by salman0butt
README.md
# Remote Job Agent MCP for ChatGPT

Version **0.3.0** is a refactored TypeScript MCP server for remote job discovery, factual ATS-CV preparation, approval-gated application packages, and application tracking.

It is intentionally designed as a **job agent**, not a LinkedIn browser bot. LinkedIn can provide lite OIDC identity/profile data and indexed job URLs can be used for discovery, while applications prefer the employer's official careers/ATS URL.

## Design goals

- **Separation of concerns:** domain rules, repositories, external adapters, services, MCP tools, and HTTP delivery live in separate modules.
- **SOLID:** services depend on narrow repositories/adapters and domain functions stay independently testable.
- **DRY:** shared parsing, canonicalization, ATS detection, screening-answer logic, and persistence are centralized.
- **KISS:** deterministic rules first, filesystem persistence for the starter, no unnecessary framework, and no hidden application automation.
- **Safe by default:** no fabricated CV facts, no authenticated LinkedIn scraping, no silent submission, and explicit application-state transitions.

## Main capabilities

### Job discovery

- Search Frontend, Backend, Full-Stack, GenAI, or custom titles together.
- Discover indexed LinkedIn listings, common ATS boards, and company career pages.
- Concurrency-limited search with request timeouts and partial-failure reporting.
- Detect ATS providers and prefer official employer/ATS URLs.
- Canonicalize URLs and merge duplicates across sources without merging different locations.
- Filter obvious remote/work-location restrictions before ranking.
- Rank with an explainable 100-point score.

### Candidate/CV data

- One factual master candidate profile.
- Separate reusable application facts such as work authorization, sponsorship need, notice period, salary expectation, relocation, availability, and years by skill.
- LinkedIn OIDC profile stored separately from the master CV.
- Tailored CV brief with anti-fabrication rules.
- Per-job Markdown CV versions; CV IDs are bound to the job that generated them.

### Application workflow

```text
Discovered
   ↓
Ranked
   ↓
Official apply URL resolved
   ↓
Tailored CV saved
   ↓
Prepared
   ↓
Screening answers reviewed
   ↓
Approved
   ↓
Submitted externally
   ↓
Interview / Rejected / Offer / Withdrawn
```

`prepared -> submitted` is intentionally invalid. A package must be explicitly approved first. Editing an approved package invalidates its approval and returns it to `prepared`.

## Architecture

```text
src/
  server.ts                    process bootstrap only
  http-server.ts               HTTP/MCP transport + request security
  mcp.ts                       MCP tool schemas and tool wiring
  types.ts                     shared domain contracts
  application-domain.ts        pure application state/answer rules
  job-intelligence.ts          pure eligibility/ranking/dedupe rules
  discovery.ts                 search-provider adapter/query builder
  linkedin-oauth.ts            LinkedIn OIDC adapter/token protection
  storage.ts                   atomic JSON persistence primitive
  repositories.ts              persistence boundaries
  services/
    job-service.ts             job use-cases/orchestration
    candidate-service.ts       candidate/CV use-cases
    application-service.ts     application use-cases/state transitions
```

See `docs/ARCHITECTURE.md` for the responsibility boundaries.

## Weighted job score

```text
Skills match             30
Experience match         20
Remote eligibility       15
Role relevance           10
Seniority                10
AI/domain relevance       5
Salary                     5
Freshness                  5
                         ---
                         100
```

Salary is currently neutral until structured salary extraction is added. The score is a prioritization aid, not a hiring prediction.

## Remote eligibility behavior

Examples:

```text
Remote worldwide / work from anywhere   -> eligible
Remote within United States              -> blocked unless explicitly eligible
Must reside in EU / Europe               -> blocked unless explicitly eligible
Remote EMEA                              -> eligible only when EMEA is explicitly allowed
On-site only                             -> blocked for remote-only candidates
Security-clearance/citizenship blocker   -> blocked when detected
Remote with unclear scope                -> manual verification
```

`worldwide` does **not** automatically imply eligibility for an EMEA-only, Europe-only, or country-restricted role.

Configure factual eligibility in `data/profile.json` or with `update_master_profile`.

## LinkedIn OAuth

LinkedIn OIDC is optional and is only used for the connected member's lite identity/profile. The MCP requests:

```text
openid profile email
```

It does **not** turn into a general LinkedIn job-search or Easy Apply API. This project does not store LinkedIn passwords/cookies, scrape authenticated pages, bypass CAPTCHAs, or click Easy Apply automatically.

Use the MCP tool:

```text
get_linkedin_connect_url
```

Open the returned URL, approve LinkedIn, and LinkedIn redirects to:

```text
/oauth/linkedin/callback
```

The callback stores the access token encrypted with AES-256-GCM and stores the lite profile separately. There is no public profile-status route.

See `docs/LINKEDIN_OAUTH.md`.

## Main MCP tools

### Discovery

- `search_best_jobs`
- `search_jobs`
- `import_job`
- `list_jobs`
- `analyze_job`
- `find_official_apply_url`
- `get_application_route`
- `get_search_config`
- `update_search_config`

### Candidate and CV

- `get_master_profile`
- `update_master_profile`
- `get_candidate_facts`
- `update_candidate_facts`
- `create_cv_brief`
- `save_cv_version`

### LinkedIn

- `get_linkedin_connect_url`
- `get_linkedin_connection`
- `disconnect_linkedin`

### Application lifecycle

- `prepare_application`
- `update_application_package`
- `approve_application`
- `record_application`
- `list_applications`
- `get_application_analytics`

`record_application` only accepts externally observed lifecycle states (`submitted`, `interview`, `rejected`, `offer`, `withdrawn`); it cannot manufacture the internal `prepared` or `approved` states.

## Configuration

Copy the example environment file:

```bash
cp .env.example .env
```

Important variables:

```bash
PORT=8787
HOST=127.0.0.1
PUBLIC_BASE_URL=http://localhost:8787

SERPER_API_KEY=
MCP_BEARER_TOKEN=use-at-least-24-random-characters-for-public-bind

REQUEST_TIMEOUT_MS=15000
DISCOVERY_CONCURRENCY=4

LINKEDIN_CLIENT_ID=
LINKEDIN_CLIENT_SECRET=
LINKEDIN_REDIRECT_URI=http://localhost:8787/oauth/linkedin/callback
TOKEN_ENCRYPTION_KEY=use-at-least-32-random-characters
```

Invalid numeric configuration fails at startup. The server refuses a non-local bind unless `MCP_BEARER_TOKEN` is configured. The bearer token is a development/single-user safeguard; use proper MCP OAuth and per-user storage before a real multi-user deployment.

## Search provider

Discovery currently uses Serper as a search-index adapter instead of scraping LinkedIn:

```bash
SERPER_API_KEY=...
```

The adapter is isolated in `src/discovery.ts`, so another permitted provider can replace it without changing domain/services/MCP tools.

## Candidate facts

`data/profile.json` is the factual CV source of truth.

`data/candidate-facts.json` stores reusable application facts:

```json
{
  "noticePeriod": "",
  "salaryExpectation": "",
  "workAuthorization": "",
  "requiresVisaSponsorship": "",
  "relocation": "",
  "availability": "",
  "yearsBySkill": {},
  "reusableAnswers": {}
}
```

The agent must not infer missing values.

## Setup

Requires Node.js 20+.

```bash
npm install
npm run check
npm run build
npm run dev
```

Endpoints:

```text
GET  /       health metadata
*    /mcp    MCP HTTP endpoint
GET  /oauth/linkedin/callback   LinkedIn OAuth callback only
```

For local ChatGPT testing, expose the server through HTTPS and connect the HTTPS `/mcp` URL.

## Docker

```bash
docker build -t remote-job-agent-mcp .
docker run --rm \
  -p 8787:8787 \
  --env-file .env \
  -e HOST=0.0.0.0 \
  -e MCP_BEARER_TOKEN=replace-with-at-least-24-random-characters \
  -v "$(pwd)/data:/app/data" \
  remote-job-agent-mcp
```

`HOST=0.0.0.0` is required inside the container for port publishing; because that is a public bind, the starter also requires a sufficiently long bearer token. Candidate profile/facts and OAuth/token files are excluded from the Docker build context so personal data is not accidentally baked into an image. Mount `data/` at runtime when persistence is needed.

## Persistence and concurrency

The starter uses JSON files for a simple single-user deployment. Writes are serialized per file and use temp-file + atomic rename so concurrent MCP requests do not overwrite/corrupt JSON state. Malformed JSON is surfaced as an error rather than silently being replaced with empty data.

For multi-user production, replace the repository implementations with PostgreSQL while keeping the service/domain APIs unchanged.

## Tests and checks

```bash
npm run typecheck
npm test
npm run check
```

Current core tests cover:

- application approval/state transitions;
- sponsorship vs work-authorization answers;
- discovery-query deduplication;
- ATS hostname detection;
- role-family classification;
- worldwide/US/EMEA eligibility behavior;
- canonical URL cleanup;
- cross-source duplicate behavior;
- official apply-link confidence;
- malformed JSON handling;
- concurrent JSON updates.

## Production upgrades

The clean extension points are intentional. Recommended next steps are PostgreSQL repositories keyed by authenticated MCP user, full MCP OAuth, permitted direct job-board adapters, job-description enrichment, DOCX/PDF rendering, Gmail response synchronization, audit logging/rate limits, and explicitly authorized ATS submission adapters where the provider/employer allows them.