Romanian Job Search MCP
README.md
# Romanian Job Search MCP
Cache-first MCP server for Romanian job search. The first provider is Jooble, with a database ledger so Hermes can search locally most of the time and spend Jooble API requests only when asked.
## Design
- `search_logged_jobs` searches the local DB only. It never calls Jooble.
- `search_and_log_jooble_jobs` can spend one Jooble API request, then upserts every returned job.
- `start_job_search_session` lets Hermes pass a Jooble key from the agent-owned `.env`.
- Raw API keys are not stored in the database. Request logs store only `key_id` and a fingerprint.
- `mark_job_interest` stores interest/application state so the LLM does not need to remember.
- `get_job_detail_schema` defines the stable structure Hermes should fill after researching a job page.
- `populate_job_details` persists that LLM-filled structured detail record.
## Key Ownership
Recommended deployment: keep Jooble keys in the Hermes/agent `.env`, not in the MCP container/appdata. Hermes should read its fallback keys and pass the selected key as `api_key` when it calls `start_job_search_session`.
The MCP keeps that raw key only in process memory for the session. The database stores request counts with a key id/fingerprint, not the secret. Named MCP-side keys via `JOOBLE_API_KEYS` still exist as an optional mode, mainly for standalone testing or an MCP-owned scheduled worker.
## MCP Tools
- `start_job_search_session(api_key, request_budget?)`
- `quota_status(session_id?, key_id?)`
- `search_logged_jobs(keywords?, location?, marked_status?)`
- `search_and_log_jooble_jobs(session_id, keywords, location?, radius?, salary?, page?, result_on_page?)`
- `get_logged_job(job_id?, source_job_id?, include_raw?)`
- `get_job_detail_schema()`
- `populate_job_details(job_id, details, extracted_by?)`
- `mark_job_interest(job_id, status, notes?)`
- `list_marked_jobs(status?)`
- `create_saved_search(...)`
- `refresh_saved_search(session_id, saved_search_id)`
## Hermes Usage Pattern
1. Start a session with a key from the Hermes `.env`:
```json
{"api_key": "${JOOBLE_RO_PRIMARY}", "request_budget": 3}
```
If the primary key hits quota, Hermes can start a new session with its fallback key.
2. Search locally first:
```json
{"keywords": "python backend", "location": "Bucuresti"}
```
3. Spend Jooble quota only if local results are missing or stale:
```json
{
"session_id": "returned-session-id",
"keywords": "python backend",
"location": "Bucuresti",
"result_on_page": 50
}
```
4. Mark jobs instead of remembering them in the LLM context:
```json
{"job_id": 1, "status": "interested", "notes": "Remote-friendly Python API role"}
```
5. For deeper details, Hermes should inspect the public job page itself, then save a structured extraction:
```json
{
"job_id": 1,
"details": {
"job_title": "Python Developer",
"company_name": "Example SRL",
"company_website": null,
"location": "Bucuresti",
"work_mode": "hybrid",
"employment_type": "full_time",
"seniority": "mid",
"contract_type": "CIM",
"salary_text": null,
"salary_min": null,
"salary_max": null,
"salary_currency": null,
"description": "Concise role description extracted from the public page.",
"responsibilities": ["Build APIs", "Maintain integrations"],
"requirements": ["Python", "REST APIs"],
"nice_to_have": [],
"benefits": [],
"technologies": ["Python", "FastAPI"],
"languages": ["English"],
"application_url": "https://ro.jooble.org/jdp/...",
"source_url": "https://ro.jooble.org/jdp/...",
"contact": null,
"evidence": {
"work_mode": "Short supporting note or snippet"
},
"missing_fields": ["salary_text"],
"confidence": 82
},
"extracted_by": "hermes"
}
```
Use `get_job_detail_schema` when Hermes needs the exact current field contract. Unknown fields are rejected; unknown values should be `null` or `[]` and listed in `missing_fields`.
## Local Development
```powershell
py -m venv .venv
.\.venv\Scripts\python.exe -m pip install -e ".[test]"
.\.venv\Scripts\python.exe -m pytest
.\.venv\Scripts\python.exe -m uvicorn ro_job_search_mcp.main:app --reload
```
The default Docker Compose MCP endpoint is `http://localhost:8012/mcp`, and health is `http://localhost:8012/health`.
## Docker Compose / Unraid
1. Copy `.env.example` to `.env`.
2. Do not put Jooble keys in this `.env` when Hermes owns them.
3. On Unraid, set `APPDATA_PATH=/mnt/user/appdata/ro-job-search-mcp`.
4. Start:
```bash
docker compose up -d --build
```
The MCP server listens on host port `${MCP_PORT:-8012}` and container port `8000`.
The `worker` service is behind the Docker Compose `worker` profile because it needs an MCP-side key. In the agent-owned key model, let Hermes call `refresh_saved_search` instead of running the worker.
## Publishing Docker Images
GitHub Actions publishes the Docker image to GitHub Container Registry:
```text
ghcr.io/ispas-catalin/ro-job-search-mcp
```
Publish by pushing a version tag:
```powershell
git tag v0.1.0
git push origin v0.1.0
```
You can also run the `Docker` workflow manually from the GitHub Actions tab.
## Jooble Quota Policy
Default limits are set for a 500-request key:
- monthly hard limit: `500`
- monthly soft warning: `430`
- daily limit: `16`
- default cache TTL: `12h`
Cached searches do not spend quota. Forced refreshes and stale cache refreshes do.
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues