eleman-net-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ permissions:
11
+ id-token: write # required for PyPI trusted publishing (OIDC)
12
+ contents: read
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+ - run: pip install build
19
+ - run: python -m build
20
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,61 @@
1
+ name: Publish to MCP Registry
2
+
3
+ # Official MCP Registry (registry.modelcontextprotocol.io) via GitHub OIDC.
4
+ # No secret, no interactive login: the runner's OIDC token proves ownership
5
+ # of the io.github.mohammad-abbasi-sh/* namespace.
6
+ # Refs: https://github.com/modelcontextprotocol/registry
7
+
8
+ on:
9
+ push:
10
+ tags: ["v*"]
11
+ workflow_dispatch:
12
+
13
+ jobs:
14
+ publish:
15
+ runs-on: ubuntu-latest
16
+ permissions:
17
+ id-token: write # required for OIDC -> MCP Registry
18
+ contents: read
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - name: Sync server.json version to tag
23
+ # Only sync when running on a version tag (v*). On workflow_dispatch
24
+ # (ref = refs/heads/main) keep the version committed in server.json.
25
+ run: |
26
+ VER=""
27
+ if [[ "$GITHUB_REF" == refs/tags/* ]]; then
28
+ TAG="${GITHUB_REF#refs/tags/}"
29
+ VER="${TAG#v}"
30
+ fi
31
+ if [ -n "$VER" ]; then
32
+ python3 - "$VER" << 'EOF'
33
+ import json, sys
34
+ ver = sys.argv[1]
35
+ d = json.load(open('server.json'))
36
+ d['version'] = ver
37
+ for pkg in d.get('packages', []):
38
+ pkg['version'] = ver
39
+ json.dump(d, open('server.json', 'w'), indent=2, ensure_ascii=False)
40
+ print('server.json version ->', ver)
41
+ EOF
42
+ else
43
+ echo "No version tag ($GITHUB_REF); keeping server.json version as-is"
44
+ fi
45
+
46
+ - name: Install mcp-publisher
47
+ run: |
48
+ curl -sL "https://github.com/modelcontextprotocol/registry/releases/download/v1.8.7/mcp-publisher_linux_amd64.tar.gz" -o /tmp/mcp-publisher.tar.gz
49
+ tar -xzf /tmp/mcp-publisher.tar.gz -C /tmp
50
+ sudo mv /tmp/mcp-publisher /usr/local/bin/mcp-publisher
51
+ mcp-publisher --version
52
+
53
+ - name: Validate server.json
54
+ run: mcp-publisher validate server.json
55
+
56
+ - name: Publish to MCP Registry (OIDC)
57
+ run: |
58
+ mcp-publisher login github-oidc
59
+ mcp-publisher publish server.json
60
+ # A registry outage must never break the release; re-run from Actions if needed.
61
+ continue-on-error: true
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mohammad Abbasi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,89 @@
1
+ Metadata-Version: 2.5
2
+ Name: eleman-net-mcp
3
+ Version: 0.1.0
4
+ Summary: Unofficial read-only MCP server for eleman.net jobs (search, job detail, sectors, cities, newest listings)
5
+ Author: Mohammad Abbasi
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: curl-cffi>=0.7
10
+ Requires-Dist: mcp<2
11
+ Description-Content-Type: text/markdown
12
+
13
+ # eleman-net-mcp 🇹🇷
14
+
15
+ mcp-name: io.github.mohammad-abbasi-sh/eleman-net-mcp
16
+
17
+ Unofficial **read-only** MCP server for eleman.net, Türkiye's blue-collar job board (#2 jobs site in Türkiye, ~4.4–5.1M visits/month per SimilarWeb) — search jobs by position or city, read full job details, list all 93 sectors and all 81 provinces, and fetch the newest listings.
18
+
19
+ > Built by Mohammad Abbasi as part of the "Turkey MCP factory" (Sepehr-style pipeline). Not affiliated with eleman.net.
20
+
21
+ ## For AI agents
22
+
23
+ Add to your MCP client config (stdio):
24
+
25
+ ```json
26
+ {
27
+ "mcpServers": {
28
+ "eleman-net": {
29
+ "command": "uvx",
30
+ "args": ["eleman-net-mcp"]
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ Or run locally: `python -m eleman_net_mcp.server` (needs `mcp<2`, `curl-cffi`).
37
+
38
+ ## Tools
39
+
40
+ | Tool | Description |
41
+ |------|-------------|
42
+ | `eleman_search_jobs` | Search jobs — `keyword` (position, e.g. "garson", "şoför", "muhasebe"), `city` (e.g. "İstanbul", "Ankara"), `page` 1.., `limit` 1..30; returns jobs (id, title, company, location, snippet, URL) + pagination (197 pages on 2026-11-07) |
43
+ | `eleman_get_job` | Full detail for one job — `job_url` (the `…/is-ilani/<slug>-i<id>` URL from a search result); returns title, date posted, employment type, full description, company (+ URL), location, region, education requirements |
44
+ | `eleman_list_sectors` | All 93 job sectors on eleman.net (name + sector id), parsed from the site's own listing sidebar |
45
+ | `eleman_list_cities` | All 81 Turkish provinces (+ the two İstanbul sides) with the eleman.net listing URL for each |
46
+ | `eleman_new_listings` | The newest listings (first page, newest first), `limit` 1..30 |
47
+
48
+ Example output (`eleman_search_jobs` / keyword "garson"):
49
+
50
+ ```json
51
+ {"keyword": "garson", "count": 2, "jobs": [
52
+ {"title": "Garson", "company": "TAVUK DÜNYASI",
53
+ "location": "İstanbul Anadolu - Ataşehir",
54
+ "url": "https://www.eleman.net/is-ilani/…"}, …]}
55
+ ```
56
+
57
+ ## How it works
58
+
59
+ eleman.net serves its listings fully server-rendered — no login, no API key, no session cookies. Listing pages carry 30 job cards each (`ilan_id_<id>` blocks: title, company, "City - District", snippet; urgent jobs carry an "ACİL İLAN" badge inside the title block and some cards carry a benefits chip span before the description — the parser handles both). Position and city pages share one slug namespace (`/is-ilanlari/<slug>`: garson, istanbul, ankara …); city + keyword combines the city slug with `?aranan=<keyword>&arandi=e`. Job detail pages carry a schema.org JobPosting JSON-LD block (title, datePosted, description, employmentType, location, company). A bare numeric id is not a valid detail URL — pass the full job URL from a search result (unknown slugs 301-redirect to the homepage, which the parser rejects). Transport uses Chrome TLS impersonation (`curl-cffi`, `chrome120`); requests are spaced ~0.8s apart; everything is read-only.
60
+
61
+ Note: the site's own `/ara` search and the forum-style search are token-gated, so keyword search goes through the slug/param paths above, which are verified live. The autocomplete endpoint (`/arama_complete.php`) is disallowed in robots.txt and is not used.
62
+
63
+ ## Install
64
+
65
+ ```bash
66
+ uvx eleman-net-mcp # run directly (needs uv)
67
+ # or
68
+ pip install eleman-net-mcp
69
+ eleman-net-mcp # starts the MCP server on stdio
70
+ ```
71
+
72
+ ## Tests
73
+
74
+ ```bash
75
+ python -m venv .venv && .venv/bin/pip install -e .
76
+ .venv/bin/python -m unittest discover -s tests -v
77
+ ```
78
+
79
+ Offline tests run on recorded fixtures (real pages fetched 2026-11-07); live tests hit eleman.net. 28/28 green at build time (15 offline + 13 live) plus an MCP stdio smoke test.
80
+
81
+ ## Roadmap
82
+
83
+ - Sector-filtered search (`?sek[]=<id>` filter)
84
+ - Company pages (`/firma/<slug>`: company jobs + profile)
85
+ - Salary info where the site publishes it (rare in the JobPosting data)
86
+
87
+ ## License
88
+
89
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,77 @@
1
+ # eleman-net-mcp 🇹🇷
2
+
3
+ mcp-name: io.github.mohammad-abbasi-sh/eleman-net-mcp
4
+
5
+ سرور MCP غیررسمی و **فقط-خواندنی** برای eleman.net، سایت کاریابی یقه‌آبی ترکیه (رتبهٔ ۲ دستهٔ مشاغل ترکیه، ‎~۴٫۴ تا ۵٫۱ میلیون بازدید در ماه طبق SimilarWeb) — جست‌وجوی شغل بر اساس سمت یا شهر، خواندن جزئیات کامل آگهی، فهرست هر ۹۳ بخش و هر ۸۱ استان، و دریافت تازه‌ترین آگهی‌ها.
6
+
7
+ > ساخته‌شده توسط محمد عباسی در چارچوب «کارخانهٔ MCP ترکیه» (خط تولید به سبک سپهر). وابسته به eleman.net نیست.
8
+
9
+ ## برای ایجنت‌های هوش مصنوعی
10
+
11
+ به پیکربندی MCP client خود اضافه کنید (stdio):
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "eleman-net": {
17
+ "command": "uvx",
18
+ "args": ["eleman-net-mcp"]
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ یا به‌صورت محلی اجرا کنید: `python -m eleman_net_mcp.server` (نیازمند `mcp<2` و `curl-cffi`).
25
+
26
+ ## ابزارها
27
+
28
+ | ابزار | توضیح |
29
+ |------|-------|
30
+ | `eleman_search_jobs` | جست‌وجوی شغل — `keyword` (سمت، مثلاً «garson»، «şoför»، «muhasebe»)، `city` (مثلاً «İstanbul»، «Ankara»)، `page` از ۱، `limit` ۱ تا ۳۰؛ آگهی‌ها (شناسه، عنوان، شرکت، مکان، خلاصه، URL) + صفحه‌بندی (۱۹۷ صفحه در ۲۰۲۶-۱۱-۰۷) |
31
+ | `eleman_get_job` | جزئیات کامل یک آگهی — `job_url` (نشانی `…/is-ilani/<slug>-i<id>` از نتیجهٔ جست‌وجو)؛ عنوان، تاریخ انتشار، نوع اشتغال، توضیح کامل، شرکت (+ URL)، مکان، منطقه، شرایط تحصیلی |
32
+ | `eleman_list_sectors` | هر ۹۳ بخش شغلی eleman.net (نام + شناسهٔ بخش)، از نوار کناری خود سایت |
33
+ | `eleman_list_cities` | هر ۸۱ استان ترکیه (+ دو سمت استانبول) با URL فهرست eleman.net برای هر یک |
34
+ | `eleman_new_listings` | تازه‌ترین آگهی‌ها (صفحهٔ اول، از جدید به قدیم)، `limit` ۱ تا ۳۰ |
35
+
36
+ خروجی نمونه (`eleman_search_jobs` / «garson»):
37
+
38
+ ```json
39
+ {"keyword": "garson", "count": 2, "jobs": [
40
+ {"title": "Garson", "company": "TAVUK DÜNYASI",
41
+ "location": "İstanbul Anadolu - Ataşehir",
42
+ "url": "https://www.eleman.net/is-ilani/…"}, …]}
43
+ ```
44
+
45
+ ## نحوهٔ کار
46
+
47
+ eleman.net آگهی‌ها را کاملاً سمت سرور رندر می‌کند — بدون ورود، کلید API و کوکی نشست. هر صفحهٔ فهرست ۳۰ کارت شغلی دارد (بلوک‌های `ilan_id_<id>`: عنوان، شرکت، «شهر - ناحیه»، خلاصه؛ آگهی‌های فوری نشان «ACİL İLAN» در بلوک عنوان دارند و بعضی کارت‌ها پیش از توضیح، برچسب مزایا دارند — تجزیه‌کننده هر دو را مدیریت می‌کند). صفحه‌های سمت و شهر یک فضای slug مشترک دارند (`/is-ilanlari/<slug>`: garson، istanbul، ankara و…)؛ ترکیب شهر + کلیدواژه، slug شهر را با `?aranan=<keyword>&arandi=e` ترکیب می‌کند. صفحه‌های جزئیات آگهی بلوک JobPosting از نوع JSON-LD از schema.org دارند (عنوان، datePosted، توضیح، employmentType، مکان، شرکت). شناسهٔ عددی تنها، URL معتبر جزئیات نیست — نشانی کامل آگهی را از نتیجهٔ جست‌وجو بدهید (slugهای ناشناخته با ۳۰۱ به صفحهٔ اصلی هدایت می‌شوند و تجزیه‌کننده آن را رد می‌کند). انتقال با جعل TLS کروم انجام می‌شود (`curl-cffi`، `chrome120`)؛ درخواست‌ها با فاصلهٔ ‎~۰٫۸ ثانیه فرستاده می‌شوند؛ همه‌چیز فقط-خواندنی است.
48
+
49
+ توجه: جست‌وجوی `/ara` خود سایت و جست‌وجوی انجمن نیازمند توکن‌اند؛ پس جست‌وجوی کلیدواژه از مسیرهای slug/پارامتر بالا که به‌صورت زنده تأیید شده‌اند استفاده می‌کند. نقطهٔ پایانی تکمیل خودکار (`/arama_complete.php`) در robots.txt ممنوع است و استفاده نمی‌شود.
50
+
51
+ ## نصب
52
+
53
+ ```bash
54
+ uvx eleman-net-mcp # اجرای مستقیم (نیازمند uv)
55
+ # یا
56
+ pip install eleman-net-mcp
57
+ eleman-net-mcp # سرور MCP را روی stdio راه می‌اندازد
58
+ ```
59
+
60
+ ## تست‌ها
61
+
62
+ ```bash
63
+ python -m venv .venv && .venv/bin/pip install -e .
64
+ .venv/bin/python -m unittest discover -s tests -v
65
+ ```
66
+
67
+ تست‌های آفلاین روی صفحات ضبط‌شده اجرا می‌شوند (صفحات واقعی دریافت‌شده در ۲۰۲۶-۱۱-۰۷)؛ تست‌های زنده به eleman.net وصل می‌شوند. در زمان ساخت ۲۸ از ۲۸ سبز بود (۱۵ آفلاین + ۱۳ زنده) به‌همراه یک تست دود MCP روی stdio.
68
+
69
+ ## نقشهٔ راه
70
+
71
+ - جست‌وجوی فیلترشده بر اساس بخش (فیلتر `?sek[]=<id>`)
72
+ - صفحه‌های شرکت (`/firma/<slug>`: آگهی‌ها + پروفایل شرکت)
73
+ - اطلاعات حقوق در جاهایی که سایت منتشر می‌کند (در دادهٔ JobPosting نادر است)
74
+
75
+ ## مجوز
76
+
77
+ MIT — ببینید [LICENSE](LICENSE).
@@ -0,0 +1,77 @@
1
+ # eleman-net-mcp 🇹🇷
2
+
3
+ mcp-name: io.github.mohammad-abbasi-sh/eleman-net-mcp
4
+
5
+ eleman.net için resmi olmayan **salt okunur** MCP sunucusu — Türkiye'nin mavi yaka iş ilanı sitesi (Türkiye'de iş kategorisinde 2. sırada, SimilarWeb'e göre ayda ~4,4–5,1 milyon ziyaret): pozisyona veya şehre göre iş arama, iş detaylarını okuma, 93 sektörün ve 81 ilin tamamını listeleme ve en yeni ilanları getirme.
6
+
7
+ > Mohammad Abbasi tarafından "Turkey MCP factory" (Sepehr tarzı hat) kapsamında geliştirildi. eleman.net ile bağlantısı yoktur.
8
+
9
+ ## Yapay zekâ ajanları için
10
+
11
+ MCP istemci yapılandırmanıza ekleyin (stdio):
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "eleman-net": {
17
+ "command": "uvx",
18
+ "args": ["eleman-net-mcp"]
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ Veya yerel çalıştırın: `python -m eleman_net_mcp.server` (`mcp<2`, `curl-cffi` gerekir).
25
+
26
+ ## Araçlar
27
+
28
+ | Araç | Açıklama |
29
+ |------|----------|
30
+ | `eleman_search_jobs` | İş arama — `keyword` (pozisyon, örn. "garson", "şoför", "muhasebe"), `city` (örn. "İstanbul", "Ankara"), `page` 1.., `limit` 1..30; işler (id, başlık, firma, konum, özet, URL) + sayfalama (2026-11-07'de 197 sayfa) |
31
+ | `eleman_get_job` | Tek bir işin tüm detayı — `job_url` (arama sonucundaki `…/is-ilani/<slug>-i<id>` URL'si); başlık, yayın tarihi, çalışma şekli, tam açıklama, firma (+ URL), konum, bölge, eğitim şartları |
32
+ | `eleman_list_sectors` | eleman.net'teki 93 sektörün tamamı (ad + sektör id), sitenin kendi listeleme kenar çubuğundan |
33
+ | `eleman_list_cities` | 81 ilin tamamı (+ İstanbul'un iki yakası) ve her biri için eleman.net listeleme URL'si |
34
+ | `eleman_new_listings` | En yeni ilanlar (ilk sayfa, en yeniden), `limit` 1..30 |
35
+
36
+ Örnek çıktı (`eleman_search_jobs` / "garson"):
37
+
38
+ ```json
39
+ {"keyword": "garson", "count": 2, "jobs": [
40
+ {"title": "Garson", "company": "TAVUK DÜNYASI",
41
+ "location": "İstanbul Anadolu - Ataşehir",
42
+ "url": "https://www.eleman.net/is-ilani/…"}, …]}
43
+ ```
44
+
45
+ ## Nasıl çalışıyor?
46
+
47
+ eleman.net ilanları tamamen sunucu taraflı render ediyor — giriş, API anahtarı ve oturum çerezi gerekmiyor. Listeleme sayfalarında sayfa başına 30 iş kartı var (`ilan_id_<id>` blokları: başlık, firma, "Şehir - İlçe", özet; acil işlerde başlık bloğunda "ACİL İLAN" rozeti, bazı kartlarda açıklama öncesinde yan hak rozeti bulunur — ayrıştırıcı ikisini de işler). Pozisyon ve şehir sayfaları tek bir slug alanını paylaşır (`/is-ilanlari/<slug>`: garson, istanbul, ankara …); şehir + anahtar kelime, şehir slug'ını `?aranan=<keyword>&arandi=e` ile birleştirir. İş detay sayfalarında schema.org JobPosting JSON-LD bloğu bulunur (başlık, datePosted, açıklama, employmentType, konum, firma). Yalın sayısal id geçerli bir detay URL'si değildir — arama sonucundaki tam iş URL'sini verin (bilinmeyen slug'lar ana sayfaya 301 yönlenir, ayrıştırıcı bunu reddeder). Aktarım Chrome TLS kimliğine bürünme kullanır (`curl-cffi`, `chrome120`); istekler ~0,8 sn arayla yapılır; her şey salt okunurdur.
48
+
49
+ Not: Sitenin kendi `/ara` araması ve forum araması token gerektirir; anahtar kelime araması bu nedenle yukarıdaki, canlı doğrulanan slug/parametre yollarını kullanır. Otomatik tamamlama uç noktası (`/arama_complete.php`) robots.txt'de yasaklıdır ve kullanılmaz.
50
+
51
+ ## Kurulum
52
+
53
+ ```bash
54
+ uvx eleman-net-mcp # doğrudan çalıştır (uv gerekir)
55
+ # veya
56
+ pip install eleman-net-mcp
57
+ eleman-net-mcp # MCP sunucusunu stdio üzerinde başlatır
58
+ ```
59
+
60
+ ## Testler
61
+
62
+ ```bash
63
+ python -m venv .venv && .venv/bin/pip install -e .
64
+ .venv/bin/python -m unittest discover -s tests -v
65
+ ```
66
+
67
+ Çevrimdışı testler kayıtlı örnek sayfalarla çalışır (2026-11-07'de çekilen gerçek sayfalar); canlı testler eleman.net'e bağlanır. Derleme anında 28/28 yeşil (15 çevrimdışı + 13 canlı) ve bir MCP stdio duman testi.
68
+
69
+ ## Yol haritası
70
+
71
+ - Sektöre göre filtreli arama (`?sek[]=<id>` filtresi)
72
+ - Firma sayfaları (`/firma/<slug>`: firmanın işleri + profili)
73
+ - Sitenin yayınladığı yerlerde maaş bilgisi (JobPosting verisinde nadir)
74
+
75
+ ## Lisans
76
+
77
+ MIT — bkz. [LICENSE](LICENSE).
@@ -0,0 +1,77 @@
1
+ # eleman-net-mcp 🇹🇷
2
+
3
+ mcp-name: io.github.mohammad-abbasi-sh/eleman-net-mcp
4
+
5
+ Unofficial **read-only** MCP server for eleman.net, Türkiye's blue-collar job board (#2 jobs site in Türkiye, ~4.4–5.1M visits/month per SimilarWeb) — search jobs by position or city, read full job details, list all 93 sectors and all 81 provinces, and fetch the newest listings.
6
+
7
+ > Built by Mohammad Abbasi as part of the "Turkey MCP factory" (Sepehr-style pipeline). Not affiliated with eleman.net.
8
+
9
+ ## For AI agents
10
+
11
+ Add to your MCP client config (stdio):
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "eleman-net": {
17
+ "command": "uvx",
18
+ "args": ["eleman-net-mcp"]
19
+ }
20
+ }
21
+ }
22
+ ```
23
+
24
+ Or run locally: `python -m eleman_net_mcp.server` (needs `mcp<2`, `curl-cffi`).
25
+
26
+ ## Tools
27
+
28
+ | Tool | Description |
29
+ |------|-------------|
30
+ | `eleman_search_jobs` | Search jobs — `keyword` (position, e.g. "garson", "şoför", "muhasebe"), `city` (e.g. "İstanbul", "Ankara"), `page` 1.., `limit` 1..30; returns jobs (id, title, company, location, snippet, URL) + pagination (197 pages on 2026-11-07) |
31
+ | `eleman_get_job` | Full detail for one job — `job_url` (the `…/is-ilani/<slug>-i<id>` URL from a search result); returns title, date posted, employment type, full description, company (+ URL), location, region, education requirements |
32
+ | `eleman_list_sectors` | All 93 job sectors on eleman.net (name + sector id), parsed from the site's own listing sidebar |
33
+ | `eleman_list_cities` | All 81 Turkish provinces (+ the two İstanbul sides) with the eleman.net listing URL for each |
34
+ | `eleman_new_listings` | The newest listings (first page, newest first), `limit` 1..30 |
35
+
36
+ Example output (`eleman_search_jobs` / keyword "garson"):
37
+
38
+ ```json
39
+ {"keyword": "garson", "count": 2, "jobs": [
40
+ {"title": "Garson", "company": "TAVUK DÜNYASI",
41
+ "location": "İstanbul Anadolu - Ataşehir",
42
+ "url": "https://www.eleman.net/is-ilani/…"}, …]}
43
+ ```
44
+
45
+ ## How it works
46
+
47
+ eleman.net serves its listings fully server-rendered — no login, no API key, no session cookies. Listing pages carry 30 job cards each (`ilan_id_<id>` blocks: title, company, "City - District", snippet; urgent jobs carry an "ACİL İLAN" badge inside the title block and some cards carry a benefits chip span before the description — the parser handles both). Position and city pages share one slug namespace (`/is-ilanlari/<slug>`: garson, istanbul, ankara …); city + keyword combines the city slug with `?aranan=<keyword>&arandi=e`. Job detail pages carry a schema.org JobPosting JSON-LD block (title, datePosted, description, employmentType, location, company). A bare numeric id is not a valid detail URL — pass the full job URL from a search result (unknown slugs 301-redirect to the homepage, which the parser rejects). Transport uses Chrome TLS impersonation (`curl-cffi`, `chrome120`); requests are spaced ~0.8s apart; everything is read-only.
48
+
49
+ Note: the site's own `/ara` search and the forum-style search are token-gated, so keyword search goes through the slug/param paths above, which are verified live. The autocomplete endpoint (`/arama_complete.php`) is disallowed in robots.txt and is not used.
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ uvx eleman-net-mcp # run directly (needs uv)
55
+ # or
56
+ pip install eleman-net-mcp
57
+ eleman-net-mcp # starts the MCP server on stdio
58
+ ```
59
+
60
+ ## Tests
61
+
62
+ ```bash
63
+ python -m venv .venv && .venv/bin/pip install -e .
64
+ .venv/bin/python -m unittest discover -s tests -v
65
+ ```
66
+
67
+ Offline tests run on recorded fixtures (real pages fetched 2026-11-07); live tests hit eleman.net. 28/28 green at build time (15 offline + 13 live) plus an MCP stdio smoke test.
68
+
69
+ ## Roadmap
70
+
71
+ - Sector-filtered search (`?sek[]=<id>` filter)
72
+ - Company pages (`/firma/<slug>`: company jobs + profile)
73
+ - Salary info where the site publishes it (rare in the JobPosting data)
74
+
75
+ ## License
76
+
77
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "eleman-net-mcp"
7
+ version = "0.1.0"
8
+ description = "Unofficial read-only MCP server for eleman.net jobs (search, job detail, sectors, cities, newest listings)"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "mcp<2",
14
+ "curl-cffi>=0.7",
15
+ ]
16
+ authors = [{ name = "Mohammad Abbasi" }]
17
+
18
+ [project.scripts]
19
+ eleman-net-mcp = "eleman_net_mcp.server:main"
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/eleman_net_mcp"]
@@ -0,0 +1 @@
1
+ {"$schema":"https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json","description":"Unofficial read-only MCP server for eleman.net jobs: search, detail, sectors, cities.","name":"io.github.mohammad-abbasi-sh/eleman-net-mcp","packages":[{"identifier":"eleman-net-mcp","registryBaseUrl":"https://pypi.org","registryType":"pypi","transport":{"type":"stdio"},"version":"0.1.0"}],"repository":{"source":"github","url":"https://github.com/mohammad-abbasi-sh/eleman-net-mcp"},"title":"eleman.net Jobs","version":"0.1.0","websiteUrl":"https://github.com/mohammad-abbasi-sh/eleman-net-mcp"}
@@ -0,0 +1 @@
1
+ """eleman_net_mcp — unofficial read-only MCP server for eleman.net."""
@@ -0,0 +1,31 @@
1
+ """Turkish provinces for eleman_net_mcp.
2
+
3
+ PROVINCES: the 81 provinces of Türkiye with the slug eleman.net
4
+ uses for city listing pages (/is-ilanlari/<slug>). Slugs follow the
5
+ site's deterministic pattern (Turkish-aware lowercase, ASCII fold,
6
+ spaces -> hyphens); the pattern is verified live for İstanbul,
7
+ Ankara, İzmir, Bursa, Antalya, Şanlıurfa and Diyarbakır. The two
8
+ İstanbul sides are extra entries — the site lists them separately.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ PROVINCES = [
13
+ "Adana", "Adıyaman", "Afyonkarahisar", "Ağrı", "Aksaray",
14
+ "Amasya", "Ankara", "Antalya", "Ardahan", "Artvin", "Aydın",
15
+ "Balıkesir", "Bartın", "Batman", "Bayburt", "Bilecik",
16
+ "Bingöl", "Bitlis", "Bolu", "Burdur", "Bursa", "Çanakkale",
17
+ "Çankırı", "Çorum", "Denizli", "Diyarbakır", "Düzce",
18
+ "Edirne", "Elazığ", "Erzincan", "Erzurum", "Eskişehir",
19
+ "Gaziantep", "Giresun", "Gümüşhane", "Hakkari", "Hatay",
20
+ "Iğdır", "Isparta", "İstanbul", "İzmir", "Kahramanmaraş",
21
+ "Karabük", "Karaman", "Kars", "Kastamonu", "Kayseri",
22
+ "Kırıkkale", "Kırklareli", "Kırşehir", "Kilis", "Kocaeli",
23
+ "Konya", "Kütahya", "Malatya", "Manisa", "Mardin", "Mersin",
24
+ "Muğla", "Muş", "Nevşehir", "Niğde", "Ordu", "Osmaniye",
25
+ "Rize", "Sakarya", "Samsun", "Siirt", "Sinop", "Sivas",
26
+ "Şanlıurfa", "Şırnak", "Tekirdağ", "Tokat", "Trabzon",
27
+ "Tunceli", "Uşak", "Van", "Yalova", "Yozgat", "Zonguldak",
28
+ ]
29
+
30
+ # Extra site entries (not provinces): the two İstanbul sides.
31
+ ISTANBUL_SIDES = ["İstanbul Anadolu", "İstanbul Avrupa"]
@@ -0,0 +1,89 @@
1
+ """HTTP layer for eleman_net_mcp.
2
+
3
+ eleman.net serves its job listings fully server-rendered — no login,
4
+ no API key, no session cookies. Plain HTTP 200 with a browser UA
5
+ (verified live 2026-11-07: /is-ilanlari, /is-ilanlari/garson,
6
+ /is-ilani/<slug>-i<id>, city+keyword combos). The transport still
7
+ uses curl-cffi chrome120 impersonation, the same proven pattern as
8
+ the other Turkey-factory servers. Requests are kept gentle (one at
9
+ a time, ~0.8s apart) and read-only.
10
+
11
+ Search paths (all verified live):
12
+ - /is-ilanlari — newest listings, ?sy=N pages (30 cards/page)
13
+ - /is-ilanlari/<slug> — position or city slug (garson, istanbul, ...)
14
+ - /is-ilanlari/<city-slug>?aranan=<keyword>&arandi=e — city + keyword
15
+ - /is-ilani/<slug>-i<id> — job detail (JobPosting JSON-LD)
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import time
20
+ from urllib.parse import quote, urlencode
21
+
22
+ from curl_cffi import requests as _r
23
+
24
+ BASE = "https://www.eleman.net"
25
+ TIMEOUT = 30.0
26
+ MIN_INTERVAL = 0.8 # seconds between requests — be polite
27
+
28
+ _HEADERS = {
29
+ "Accept-Language": "tr-TR,tr;q=0.9,en;q=0.8",
30
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
31
+ }
32
+
33
+ _last_fetch = 0.0
34
+
35
+
36
+ class ElemanError(RuntimeError):
37
+ pass
38
+
39
+
40
+ def _get(url: str) -> str:
41
+ global _last_fetch
42
+ wait = MIN_INTERVAL - (time.monotonic() - _last_fetch)
43
+ if wait > 0:
44
+ time.sleep(wait)
45
+ try:
46
+ resp = _r.get(url, impersonate="chrome120", headers=_HEADERS, timeout=TIMEOUT)
47
+ except Exception as e:
48
+ raise ElemanError(f"network error fetching {url}: {e}") from e
49
+ finally:
50
+ _last_fetch = time.monotonic()
51
+ if resp.status_code == 403:
52
+ raise ElemanError(
53
+ "eleman.net refused the request (HTTP 403, bot wall) — try again later"
54
+ )
55
+ if resp.status_code == 429:
56
+ raise ElemanError(
57
+ "eleman.net rate-limited the request (HTTP 429) — try again later"
58
+ )
59
+ if resp.status_code == 404:
60
+ raise ElemanError(f"page not found (HTTP 404): {url}")
61
+ if resp.status_code != 200:
62
+ raise ElemanError(f"unexpected HTTP {resp.status_code} fetching {url}")
63
+ return resp.text
64
+
65
+
66
+ def fetch_listings(path: str = "", page: int = 1, keyword: str | None = None) -> str:
67
+ """Listing page. path: '' or a position/city slug; keyword adds
68
+ ?aranan=<keyword>&arandi=e (used together with a city slug)."""
69
+ url = f"{BASE}/is-ilanlari"
70
+ if path:
71
+ url += f"/{quote(path)}"
72
+ params: dict[str, str] = {}
73
+ if keyword:
74
+ params["aranan"] = keyword
75
+ params["arandi"] = "e"
76
+ if page and page > 1:
77
+ params["sy"] = str(page)
78
+ if params:
79
+ url += "?" + urlencode(params)
80
+ return _get(url)
81
+
82
+
83
+ def fetch_job_page(path: str) -> str:
84
+ """Job detail page: /is-ilani/<slug>-i<id> (full path or URL)."""
85
+ if path.startswith("http"):
86
+ url = path
87
+ else:
88
+ url = f"{BASE}/{path.lstrip('/')}"
89
+ return _get(url)