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.
- eleman_net_mcp-0.1.0/.github/workflows/publish.yml +20 -0
- eleman_net_mcp-0.1.0/.github/workflows/registry.yml +61 -0
- eleman_net_mcp-0.1.0/LICENSE +21 -0
- eleman_net_mcp-0.1.0/PKG-INFO +89 -0
- eleman_net_mcp-0.1.0/README-fa.md +77 -0
- eleman_net_mcp-0.1.0/README-tr.md +77 -0
- eleman_net_mcp-0.1.0/README.md +77 -0
- eleman_net_mcp-0.1.0/pyproject.toml +22 -0
- eleman_net_mcp-0.1.0/server.json +1 -0
- eleman_net_mcp-0.1.0/src/eleman_net_mcp/__init__.py +1 -0
- eleman_net_mcp-0.1.0/src/eleman_net_mcp/cities.py +31 -0
- eleman_net_mcp-0.1.0/src/eleman_net_mcp/http.py +89 -0
- eleman_net_mcp-0.1.0/src/eleman_net_mcp/jobs.py +226 -0
- eleman_net_mcp-0.1.0/src/eleman_net_mcp/server.py +156 -0
- eleman_net_mcp-0.1.0/tests/__init__.py +0 -0
- eleman_net_mcp-0.1.0/tests/fixtures/detail.html +63 -0
- eleman_net_mcp-0.1.0/tests/fixtures/garson.html +128 -0
- eleman_net_mcp-0.1.0/tests/fixtures/listing.html +135 -0
- eleman_net_mcp-0.1.0/tests/test_jobs.py +144 -0
- eleman_net_mcp-0.1.0/tests/test_live.py +119 -0
|
@@ -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)
|