MCP server pro tržní inteligenci nad českými pracovními portály — 6 providerů, boolean matching, 3-vrstvý dedup, PostgreSQL perzistence, MCP rozhraní pro AI agenty, Streamlit dashboard, skills catalog (119 IT skills, v2 scoring). Nástupce legacy scrapers — 5.8× rychlejší, config-driven, 231 testů (včetně contract tests), hardening pro produkci.
- ukázka produkčního výstupu: automaticky generované html = IT domain job search output
Co to je: autonomně vyvinutý, iterativně tvrzený systém — od scraping adapteru přes normalizaci, dedup a perzistenci až po AI-facing MCP protokol. Každá vrstva (HTTP → scraping → normalizace → matcher → dedup → persistence → MCP) je oddělena přes stabilní rozhraní, takže přidání nového portálu nevyžaduje přepis centrální pipeline.
-
6 portálů — jobs.cz, prace.cz, bazos.cz, jenprace.cz, profesia.cz, volnamista.cz (LMC network, ManpowerGroup, Seznam.cz) — 4 různé anti-bot/layout obrany
-
8 konfigurovatelných query — python_ai_engineer, ai_llm_engineer, mcp_agentic, data_engineering, devops_ci_cd, prumyslova_automatizace, cnc_cam_automation, reverse_engineering
-
Boolean matching — plná AND/OR/NOT/parens logika s AST parserem + diakritika + LRU cache (8000→8 parsování)
-
3-vrstvý dedup — URL canonicalizace → fuzzy in-memory → DB-level cross-portal
-
Exclude listy — word-boundary na title, substring na description (české skloňování)
-
Location & salary filter — substring location match, regex salary extraction
-
Rate limiting — 0.5s mezi requesty (ToS compliance, clamp min 0.2s) + pages guard (max 50)
-
Auto-validace boolean výrazů — fail-fast při malformed configu
-
PostgreSQL persistence — URL UNIQUE + ON CONFLICT dedup, run audit, graceful degradation
-
SSRF hardening — domain allowlist (SEC-001), regression testy na dangerous targets
-
Structured logging — per-card skip count, 0-ads alert, žádné silent failures
-
Streamlit dashboard — v2 layout (table-first, on_select row selection, inline detail+status, sidebar KPI), modulární frontend, shared filters, pure SQL metrics, contract tests
-
Skills catalog — 119 IT skills, 27 kategorií, v2 scoring formule (coverage 60pts + log_count 30pts + diversity 10pts), per-skill weights kalibrované pro CZ trh, PostgreSQL JSONB + GIN index
src/mcp_jobs/
├── config.py # UserConfig → PortalConfig → CategoryConfig → QueryConfig
├── models.py # Ad dataclass (title, url, portal, desc, company, ...)
├── http.py # HTTP klient s retry, timeout, rate limiting
├── matcher.py # Boolean AST evaluator + LRU cache + exclude filter + strip_diacritics
├── utils.py # normalize_url, fuzzy_key — centrální dedup normalizace
├── pipeline.py # SearchPipeline orchestrator (scrape → filter → results → persist)
├── storage.py # Unified output (etl_{PROFILE}_{ts}.{json,md,html}) + dedup
├── db.py # PostgreSQL persistence (schema.sql, upsert_ads batched, run audit)
├── skills_catalog.py # SkillsCatalog — v2 scoring (coverage + log_count + diversity)
├── report.py # Report rendering (markdown/html, priority flags, match tags)
├── report_style.py # Report CSS/HTML šablony
├── providers/ # Portal-specific scrapers
│ ├── base.py # BaseScraper ABC + detail extraction helper
│ ├── bazos.py # Bazos.cz s params podporou (hlokalita, humkreis)
│ ├── jobs.py # Jobs.cz (LMC network)
│ ├── pracecz.py # Prace.cz (LMC network)
│ ├── jenprace.py # Jenprace.cz
│ ├── profesia.py # Profesia.cz (ManpowerGroup layout varianta)
│ └── volnamista.py # Volnamista.cz (Seznam.cz — anti-bot headers, JSON-LD/NEXT_DATA)
├── server.py # FastMCP instance + tool/resource/prompt registrace + async job runner
└── cli.py # CLI entry point (stdio MCP transport)
data/
├── schema.sql # DDL pro PostgreSQL (ads, pipeline_runs, UNIQUE url, fuzzy sloupce)
├── query_store.json # Persistence query výstupů
└── _archived_bazos/ # Archivované staré bazos/nyx JSON (v1 data)
output/ # ETL výstupy etl_{PROFILE}_{ts}.{json,md,html}
dashboard/ # Streamlit frontend (v2 — table-first, on_select, sidebar KPI)
├── app.py # Auth, sidebar gatekeeper + KPI, tab routing
├── filters.py # Shared sidebar filters + WHERE builder
├── metrics.py # Pure SQL analytics (12 functions, 0 Streamlit calls)
├── theme.css # Dark mode production theme
├── components/
│ ├── kpi.py # KPI cards (main + sidebar compact)
│ ├── gatekeeper.py # Pipeline status (sidebar compact)
│ └── styling.py # Shared styling utilities
└── tabs/
├── ads.py # Table-first, on_select row selection, inline detail + status change
├── analysis.py # Skills analytika, top ads, histogram
└── runs.py # Pipeline history
scripts/
├── run_etl.py # Základní ETL runner
├── run_etl_metrics.py # ETL runner s per-provider timingem
├── run_livetests.py # Live test runner (structured results do data/)
├── backfill_skills.py # Batch backfill skills (--force pro prepis v1→v2)
├── db.ps1 # DB operace (restart, psql, logy, dedup check)
└── healthcheck.py # Healthcheck (dry-mode bez DB)
docker-compose.yml # PostgreSQL 16 (volume, schema.sql init, healthcheck)
Systém je navržen tak, aby selhání byla viditelná a měřitelná, ne tichá:
-
HTTP 200 dokazuje transportní úspěch, ne sémantický. Provider telemetrie sleduje řetězec
HTTP success → cards parsed → valid ads → detail fetch → final output. Stránka s 0 kartami na page>1 = end-of-list (INFO), na page 1 = layout change (ERROR) — obojí logované rozlišitelně. -
Dedup trade-off je observovatelný. Fuzzy dedup je heuristika (title+company+location se normalizují na identitu) — kolize se logují, neschovávají se.
-
Cache ≠ persistence. Detail cache zabraňuje opakovanému HTTP work; PostgreSQL zachovává stav. Runtime job state je ztracen restartem, persisted data přežívají.
-
Bounded concurrency. Background executor izoluje long-running scraping od MCP request timeoutu; job-state model
pending → running → done/error. -
Evidenčně řízená iterace. Regresní testy vznikají kolem objevených failure mode:
identifikuj concrete failure mode → patch boundary → regression test → preserve invariant. SSRF, malformed config, dedup kolize, bot-detekce, test-ordering pollution.
Deep reasoning, dedup audit a provider resilience case studies jsou v
docs/(viz níže).
\# Instalace
python -m venv .venv
.venv\\Scripts\\activate
pip install -e ".\[dev\]"
\# Konfigurace
copy config.yaml config\_local.yaml
\# Uprav portály, query, exclude dle potřeby (viz sekce Konfigurace)
\# PostgreSQL (volitelné, pro perzistenci)
docker compose up -d
copy .env.example .env \# nastav DATABASE\_URL=postgres://mcpjobs:mcpjobs@localhost:5432/mcpjobs
\# nebo: scripts\\db.ps1 (restart, psql, logy)
\# Testy (231 testů; DB testy běží na mcpjobs_test, jinak se skipnou + 9 contract tests)
pytest tests/ -v
\# ETL pipeline
python scripts\\run\_etl.py
\# Fallback profil (legacy manual query):
python scripts\\run\_etl.py --config config\_legacy\_manual.yaml
Všechna personalizace je v config.yaml — žádná osobní data v kódu:
user: "default"
profile: "AI-NATIVE"
pipeline:
max\_workers: 3
request\_delay: 0.5
portals:
jobs:
enabled: true
categories:
- url: "https://www.jobs.cz/prace/praha/"
pages: 20
profesia:
enabled: true
categories:
- url: "https://www.profesia.cz/prace/praha/"
pages: 10
queries:
python\_ai\_engineer:
boolean: "(python AND (ai OR ml OR data OR vyvojar OR programator OR inzenyr OR engineer OR developer)) NOT (lektor OR kurz OR skoleni)"
exclude: \["agentura", "nabizim", "hledam praci"\]
locations: \["praha", "remote", "home office", "domova", "hybrid", "dálkově"\]
portals: \["jobs", "pracecz", "jenprace", "profesia", "volnamista"\]
Poznámka: Boolean parser je strict-only — nepodporuje implicitní AND.
"python developer"je nutné psát jako"python AND developer". Vizvalidate\_boolean()pro kontrolu syntaxe.
1. \_scrape\_all() → pool \[Ad\] (všechny portály, kategorie, stránky; 0.5s delay mezi requesty)
2. Pro každý query:
├── Portal filter (ad.portal IN query.portals?)
├── Boolean match (evaluate\_boolean AST — LRU cached, 1× parse per unique query)
├── Exclude filter (has\_exclude\_terms)
├── Location filter (substring)
└── Salary filter (number \>= min\_salary)
3. Dedup (URL canonical → fuzzy in-memory → DB-level cross-portal)
4. Detail fetch (parallel, bounded) — jen pro kandidáty vyžadující description
5. Vrací dict\[str, list\[Ad\]\]
| Metrika | Legacy | MCP-Jobs | Zlepšení |
|---|---|---|---|
| Pipeline time | ~210 s | ~36 s | 5.8× faster |
| Raw ads scraped | ~500 | 1 073 | 2× více dat |
| Per-provider bazos | ~80 s | ~12 s | 6.7× faster |
| Per-provider jobs | ~40 s | ~8 s | 5.0× faster |
| Per-provider pracecz | ~60 s | ~16 s | 3.8× faster |
| Unit tests | 0 | 231 (9 contract) | — |
| Matching | AND-only, first-match-wins | Boolean AST + LRU cache | plná logika |
| Diakritika | ruční mapping | NFKD normalizace | automatická |
| Error handling | except: continue (silent) |
structured logging + skip count | žádné silent failures |
| Dedup | URL only | URL + fuzzy + DB cross-portal | 3 vrstvy |
| Detail fetch | sequential | parallel (bounded) | — |
| Output | CSV+MD per portal | unified JSON/MD/HTML + PostgreSQL | dual-write |
| Protokol | žádný | MCP (tools, resources, prompts) | AI-native |
| Security | none | domain allowlist (SSRF) | hardening |
| Subdomény bazos | broken (www vs prace) | automatická detekce | — |
Každý ETL běh generuje unified output/etl\_\{PROFILE\}\_\{ts\}.\{json,md,html\} s per-provider timingem:
\{
"pipeline\_elapsed\_s": 36.3,
"total\_raw\_scraped": 1073,
"total\_final\_matched": 28,
"precision\_pct": 2.6,
"provider\_detail": \{
"bazos": \{ "elapsed\_s": 12.0, "raw\_ads": 463, "errors": 0 \},
"jobs": \{ "elapsed\_s": 8.0, "raw\_ads": 210, "errors": 0 \},
"pracecz": \{ "elapsed\_s": 16.0, "raw\_ads": 400, "errors": 0 \}
\}
\}
| Tool | Description |
|---|---|
health\_check |
Server status and version |
search\_jobs\_v2 |
Boolean search across CZ portals (pages guard: max 50) |
search\_from\_config |
Full pipeline from YAML file path |
search\_from\_yaml |
Full pipeline from inline YAML content |
search\_status |
Status of background search job (async pipeline) |
list\_portals |
Available portals and categories |
-
L2 Resources —
mcp-jobs://ads/list,mcp-jobs://ads/\{query\_id\},mcp-jobs://ads/\{query\_id\}/report -
L3 Prompts —
search\_expert(převod přirozeného jazyka → boolean query + YAML snippet) -
L4 Streaming — plánováno
119 IT skills v 27 kategoriích, kalibrovaných pro CZ trh (TIOBE 2026, ABSL Report, ManpowerGroup Q2).
coverage_score = min(60.0, math.log1p(mentioned_weighted * 10.0) * 12.0)
count_score = (math.log2(1 + skill_count) / math.log2(8)) * 30.0
diversity_score = (unique_categories / skill_count * 10.0) if skill_count > 0 else 0.0
score = min(100.0, coverage_score + count_score + diversity_score) 0 (no skills): 93 ads (51%) — ne-IT queries
1-39: 2 ads (1%)
40-59: 19 ads (10%) — dual skill
60-79: 16 ads (9%) — multi skill
80+: 53 ads (29%) — rich IT stack
| Skill | Váha | Zdroj |
|---|---|---|
| Python | 1.00 | TIOBE #1 |
| AI | 0.80 | LinkedIn +287% |
| K8s | 0.60 | LinkedIn +78% |
| LLM | 0.60 | Top trend |
| CNC | 0.60 | CZ industrial niche |
| AWS | 0.50 | Synergy 31% |
| Azure | 0.50 | Synergy 25% |
| Java | 0.50 | TIOBE #4 |
| C# | 0.50 | TIOBE #5 |
| SQL | 0.80 | TIOBE #8 |
Deep reasoning a audit artefakty jsou v docs/, README zůstává interface/overview/proof:
-
docs/edukace\_pridani\_providera\_2026-08-19.md— metodologie přidávání providerů (case studies, dedup audit, F0-F6 checklist) -
docs/sql\_ontologie\_mechanismy\_2026-08-15.md— SQL ontologie (DDL/DML/DQL, dependencies) -
docs/postgresql\_zakladni\_prikazy\_2026-08-15.md— PostgreSQL reference (psql meta, dedup vzory) -
docs/edukace\_faze1\_postgresql\_2026-08-15.md— retrospektiva Fáze 1 (PostgreSQL persistence) -
docs/l2\_resources.md— MCP L2 Resources dokumentace
-
Rate limiting: 0.5s delay = 36s pipeline. Nutný pro ToS compliance.
-
Double scrape:
SearchPipeline.run()vždy rescrapuje interně, nelze injectnout existující pool -
Description matching: word boundaries VYPNUTY na description (záměrně — české skloňování)
-
Location filter: substring matching (ne geokód), pro "Praha" dostačující
-
Salary filter: heuristická extrakce čísel (různé formáty napříč portály)
-
Fuzzy dedup: heuristika — false positive možná (firma, dvě role, stejný title+location); kolize se logují
-
Runtime job state: in-memory — restart serveru ztrácí in-flight joby
-
Security: threat model není dokumentován — nutno doplnit před veřejnou publikací
-
Domain allowlist: SEC-001 SSRF protection — 6 povolených domén (bazos.cz, jobs.cz, prace.cz, jenprace.cz, profesia.cz, volnamista.cz)
| Oblast | Stav |
|---|---|
| Verze | 0.5.0 |
| Testy | 231/231 PASS (222 unit + 9 contract), ruff 0 |
| Portály | 6/6 (jobs, pracecz, bazos, jenprace, profesia, volnamista) |
| Dedup | 3-vrstvý (URL canonical → fuzzy in-memory → DB cross-portal) |
| PostgreSQL | 183 ads, v2 scoring (coverage+log_count+diversity), backup v1 |
| Skills | 119 IT skills, 27 kategorií, v2 formule, per-skill CZ weights |
| Anti-bot | Seznam.cz consent-page fix, end-of-list detekce |
| MCP | L1-L3 (6 tools, resources, prompt), L4 plánováno |
| Dashboard | v2: table-first, on_select, inline detail+status, sidebar KPI, h3 hyperlink |
| CI | Test DB mcpjobs_test krok v ci.yml, pandas<3 pin |
| Backlog | EROI dimenze (L1), employer tiers, location_score, 84 default skill vah |