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.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues