mcp-il-professionals 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,6 @@
1
+ .git
2
+ .venv
3
+ __pycache__
4
+ .pytest_cache
5
+ .ruff_cache
6
+ tests
@@ -0,0 +1,49 @@
1
+ name: Publish to PyPI and MCP Registry
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - run: pip install -e ".[dev]"
17
+ - run: ruff check .
18
+ - run: pytest
19
+
20
+ publish:
21
+ needs: test
22
+ runs-on: ubuntu-latest
23
+ environment: pypi
24
+ permissions:
25
+ id-token: write # PyPI trusted publishing + MCP Registry GitHub OIDC
26
+ contents: read
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: actions/setup-python@v5
30
+ with:
31
+ python-version: "3.12"
32
+ - name: Check tag matches versions
33
+ if: startsWith(github.ref, 'refs/tags/v')
34
+ run: |
35
+ TAG="${GITHUB_REF_NAME#v}"
36
+ PY=$(python -c "import tomllib;print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
37
+ SJ=$(python -c "import json;print(json.load(open('server.json'))['version'])")
38
+ test "$TAG" = "$PY" && test "$TAG" = "$SJ" || { echo "tag=$TAG pyproject=$PY server.json=$SJ"; exit 1; }
39
+ - run: pip install build && python -m build
40
+ - uses: pypa/gh-action-pypi-publish@release/v1
41
+ - name: Install mcp-publisher
42
+ run: |
43
+ curl -sL "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz" | tar xz mcp-publisher
44
+ - name: Wait for PyPI to serve the new version
45
+ run: sleep 60
46
+ - name: Publish to MCP Registry
47
+ run: |
48
+ ./mcp-publisher login github-oidc
49
+ ./mcp-publisher publish
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ .env
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ dist/
8
+ build/
9
+ *.egg-info/
@@ -0,0 +1,5 @@
1
+ {
2
+ "mcpServers": {
3
+ "il-professionals": { "command": "mcp-il-professionals" }
4
+ }
5
+ }
@@ -0,0 +1,26 @@
1
+ # mcp-il-professionals — CLAUDE.md
2
+
3
+ Verify licensed professionals in Israel for AI agents: real-estate brokers, registered contractors, appraisers, CPAs, surveyors, private investigators — live from data.gov.il.
4
+
5
+ ## Stack
6
+ Python 3.11 · FastMCP · httpx · pytest. No database: every tool queries data.gov.il CKAN `datastore_search` live.
7
+
8
+ ## Layout
9
+ - server.py — Prefect Horizon entrypoint (`server.py:mcp`); check with `fastmcp inspect server.py:mcp`
10
+ - src/mcp_il_professionals/server.py — tools + resource IDs (constants `R_*` at top)
11
+ - src/mcp_il_professionals/ckan.py — shared CKAN client (retries, User-Agent, `~`→`"` cleanup, `meta()` provenance). Keep identical across mcp-il-* repos.
12
+ - tests/ — offline tests (monkeypatched `datastore_search`); `pytest -m live` for real calls
13
+
14
+ ## Rules
15
+ - Every tool response includes `source`, `retrieved_at`, `attribution` via `meta()`.
16
+ - Resource IDs can change when a ministry republishes — if a tool returns 404/empty, re-resolve with
17
+ `package_search` and update the `R_*` constant.
18
+ - Exact matches via `filters` (numbers must be int unless the dataset stores strings); free text via `q`.
19
+ - Never store or log personal queries. Data is public registry data only.
20
+ - Update MEMORY.md at the end of every session.
21
+
22
+ ## Model routing
23
+ Architect/Advisor: Fable (else Opus) · Orchestrator: Opus · Executor: Sonnet · Explorer/tests: Haiku · tiny text jobs: Jetson llama.cpp · graphics: Gemini/ChatGPT.
24
+
25
+ ## Deployment
26
+ Prefect Horizon free tier (public HTTP), entrypoint `server.py:mcp`. data.gov.il is reachable from cloud hosts.
@@ -0,0 +1,9 @@
1
+ FROM python:3.12-slim
2
+ WORKDIR /app
3
+ COPY pyproject.toml README.md LICENSE ./
4
+ COPY src ./src
5
+ RUN pip install --no-cache-dir .
6
+ ENV MCP_TRANSPORT=http PORT=8000
7
+ EXPOSE 8000
8
+ USER nobody
9
+ CMD ["mcp-il-professionals"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oleg Sher
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,14 @@
1
+ # mcp-il-professionals — MEMORY.md
2
+
3
+ ## Status (2026-11-14)
4
+ - v0.1.0 built and tested against live data.gov.il; repo https://github.com/olegsher-ds-2025/mcp-il-professionals
5
+ - PyPI + MCP Registry publishing via GitHub Actions on tag v*; Horizon free tier is private-only (public needs paid plan) → self-host on laptop 10.0.0.100 via Cloudflare Tunnel (mcp-il-deploy)
6
+
7
+ ## Next tasks
8
+ 1. Deploy to Prefect Horizon; add hosted URL to README
9
+ 2. List on MCP Registry, Glama, Smithery, agentskills.co.il
10
+ 3. Roadmap items in README
11
+
12
+ ## Facts
13
+ - data.gov.il CKAN reachable from Claude cloud workspace and cloud hosts (unlike cbs.gov.il)
14
+ - Registry text encodes `"` as `~` — cleaned in ckan.clean_value
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.5
2
+ Name: mcp-il-professionals
3
+ Version: 0.1.0
4
+ Summary: Verify licensed professionals in Israel for AI agents: real-estate brokers, registered contractors, appraisers, CPAs, surveyors, private investigators — live from data.gov.il
5
+ Project-URL: Repository, https://github.com/olegsher-ds-2025/mcp-il-professionals
6
+ Author: Oleg Sher
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Keywords: data.gov.il,israel,mcp,model-context-protocol,open-data
10
+ Requires-Python: >=3.11
11
+ Requires-Dist: fastmcp>=2.0
12
+ Requires-Dist: httpx>=0.27
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=8; extra == 'dev'
15
+ Requires-Dist: ruff>=0.5; extra == 'dev'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # mcp-il-professionals
19
+
20
+ <!-- mcp-name: io.github.olegsher-ds-2025/mcp-il-professionals -->
21
+
22
+ Verify licensed professionals in Israel for AI agents: real-estate brokers, registered contractors, appraisers, CPAs, surveyors, private investigators — live from data.gov.il.
23
+
24
+ **אימות בעלי מקצוע מורשים: מתווכים, קבלנים רשומים, שמאים, רואי חשבון, מודדים, חוקרים פרטיים**
25
+
26
+ An [MCP](https://modelcontextprotocol.io) server that lets Claude and other AI assistants query official Israeli
27
+ government open data in plain language. No API key, no scraping — every call goes to the public
28
+ [data.gov.il](https://data.gov.il) CKAN API and returns the source with each answer.
29
+
30
+ ## Tools
31
+
32
+ | Tool | What it does |
33
+ |---|---|
34
+ | `list_professions` | Which registries are available, and how to verify electricians / engineers / lawyers |
35
+ | `verify_professional` | Check a licence by name or number in one registry |
36
+ | `find_contractors` | Registered contractors by city, branch (שיפוצים, בניה, חשמלאות…), classification |
37
+ | `contractor_profile` | All branches & classifications of one contractor |
38
+ | `search_all_registries` | One name across every registry |
39
+ | `health` | Connectivity check |
40
+
41
+ ## Try asking
42
+
43
+ - “Is the real-estate broker דורית גריצר licensed?”
44
+ - “Find registered renovation contractors in Modiin recognized for government work.”
45
+ - “Check CPA licence 5005603.”
46
+ - “What licences does this person hold across all registries?”
47
+
48
+ ## Install
49
+
50
+ No install needed with [uv](https://docs.astral.sh/uv/):
51
+
52
+ ```bash
53
+ uvx mcp-il-professionals
54
+ ```
55
+
56
+ Claude Desktop / Claude Code (`.mcp.json`):
57
+
58
+ ```json
59
+ { "mcpServers": { "il-professionals": { "command": "uvx", "args": ["mcp-il-professionals"] } } }
60
+ ```
61
+
62
+ Or `pip install mcp-il-professionals`, or from source: `pip install git+https://github.com/olegsher-ds-2025/mcp-il-professionals`.
63
+
64
+ Also listed in the official MCP Registry as `io.github.olegsher-ds-2025/mcp-il-professionals`.
65
+
66
+ Run over HTTP (for remote hosting):
67
+
68
+ ```bash
69
+ MCP_TRANSPORT=http PORT=8000 mcp-il-professionals # → http://localhost:8000/mcp
70
+ ```
71
+
72
+ Docker: `docker build -t mcp-il-professionals . && docker run -p 8000:8000 mcp-il-professionals`.
73
+
74
+ Prefect Horizon entrypoint: `server.py:mcp`.
75
+
76
+ ## Data sources
77
+
78
+ - Licensed real-estate brokers (Ministry of Justice)
79
+ - Registered contractors (Registrar of Contractors)
80
+ - Real-estate appraisers
81
+ - Certified public accountants (CPA Council)
82
+ - Vehicle appraisers (Ministry of Transport)
83
+ - Licensed surveyors (Survey of Israel)
84
+ - Private investigators & investigation firms
85
+
86
+ All data © the publishing Israeli government authority, via data.gov.il. This project is independent and not
87
+ affiliated with the Government of Israel. Registry data can lag; verify critical facts with the authority.
88
+
89
+ ## Development
90
+
91
+ ```bash
92
+ pip install -e ".[dev]"
93
+ pytest # offline tests
94
+ pytest -m live # hits data.gov.il
95
+ ```
96
+
97
+ ## Roadmap
98
+
99
+ - [ ] Electricians licence search (Ministry of Labor — not open data; investigate official lookup)
100
+ - [ ] Engineers & architects registry
101
+ - [ ] Insurance agents (Capital Market Authority)
102
+ - [ ] Join contractors with mcp-il-companies due_diligence
103
+
104
+ ## Related
105
+
106
+ Part of a family of Israeli public-data MCP servers by [Oleg Sher](https://linkedin.com/in/sheroleg):
107
+ mcp-il-companies · mcp-il-vehicles · mcp-il-health · mcp-il-professionals · mcp-cbs-places · mcp-mavat.
108
+
109
+ ## License
110
+
111
+ MIT
@@ -0,0 +1,94 @@
1
+ # mcp-il-professionals
2
+
3
+ <!-- mcp-name: io.github.olegsher-ds-2025/mcp-il-professionals -->
4
+
5
+ Verify licensed professionals in Israel for AI agents: real-estate brokers, registered contractors, appraisers, CPAs, surveyors, private investigators — live from data.gov.il.
6
+
7
+ **אימות בעלי מקצוע מורשים: מתווכים, קבלנים רשומים, שמאים, רואי חשבון, מודדים, חוקרים פרטיים**
8
+
9
+ An [MCP](https://modelcontextprotocol.io) server that lets Claude and other AI assistants query official Israeli
10
+ government open data in plain language. No API key, no scraping — every call goes to the public
11
+ [data.gov.il](https://data.gov.il) CKAN API and returns the source with each answer.
12
+
13
+ ## Tools
14
+
15
+ | Tool | What it does |
16
+ |---|---|
17
+ | `list_professions` | Which registries are available, and how to verify electricians / engineers / lawyers |
18
+ | `verify_professional` | Check a licence by name or number in one registry |
19
+ | `find_contractors` | Registered contractors by city, branch (שיפוצים, בניה, חשמלאות…), classification |
20
+ | `contractor_profile` | All branches & classifications of one contractor |
21
+ | `search_all_registries` | One name across every registry |
22
+ | `health` | Connectivity check |
23
+
24
+ ## Try asking
25
+
26
+ - “Is the real-estate broker דורית גריצר licensed?”
27
+ - “Find registered renovation contractors in Modiin recognized for government work.”
28
+ - “Check CPA licence 5005603.”
29
+ - “What licences does this person hold across all registries?”
30
+
31
+ ## Install
32
+
33
+ No install needed with [uv](https://docs.astral.sh/uv/):
34
+
35
+ ```bash
36
+ uvx mcp-il-professionals
37
+ ```
38
+
39
+ Claude Desktop / Claude Code (`.mcp.json`):
40
+
41
+ ```json
42
+ { "mcpServers": { "il-professionals": { "command": "uvx", "args": ["mcp-il-professionals"] } } }
43
+ ```
44
+
45
+ Or `pip install mcp-il-professionals`, or from source: `pip install git+https://github.com/olegsher-ds-2025/mcp-il-professionals`.
46
+
47
+ Also listed in the official MCP Registry as `io.github.olegsher-ds-2025/mcp-il-professionals`.
48
+
49
+ Run over HTTP (for remote hosting):
50
+
51
+ ```bash
52
+ MCP_TRANSPORT=http PORT=8000 mcp-il-professionals # → http://localhost:8000/mcp
53
+ ```
54
+
55
+ Docker: `docker build -t mcp-il-professionals . && docker run -p 8000:8000 mcp-il-professionals`.
56
+
57
+ Prefect Horizon entrypoint: `server.py:mcp`.
58
+
59
+ ## Data sources
60
+
61
+ - Licensed real-estate brokers (Ministry of Justice)
62
+ - Registered contractors (Registrar of Contractors)
63
+ - Real-estate appraisers
64
+ - Certified public accountants (CPA Council)
65
+ - Vehicle appraisers (Ministry of Transport)
66
+ - Licensed surveyors (Survey of Israel)
67
+ - Private investigators & investigation firms
68
+
69
+ All data © the publishing Israeli government authority, via data.gov.il. This project is independent and not
70
+ affiliated with the Government of Israel. Registry data can lag; verify critical facts with the authority.
71
+
72
+ ## Development
73
+
74
+ ```bash
75
+ pip install -e ".[dev]"
76
+ pytest # offline tests
77
+ pytest -m live # hits data.gov.il
78
+ ```
79
+
80
+ ## Roadmap
81
+
82
+ - [ ] Electricians licence search (Ministry of Labor — not open data; investigate official lookup)
83
+ - [ ] Engineers & architects registry
84
+ - [ ] Insurance agents (Capital Market Authority)
85
+ - [ ] Join contractors with mcp-il-companies due_diligence
86
+
87
+ ## Related
88
+
89
+ Part of a family of Israeli public-data MCP servers by [Oleg Sher](https://linkedin.com/in/sheroleg):
90
+ mcp-il-companies · mcp-il-vehicles · mcp-il-health · mcp-il-professionals · mcp-cbs-places · mcp-mavat.
91
+
92
+ ## License
93
+
94
+ MIT
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mcp-il-professionals"
7
+ version = "0.1.0"
8
+ description = "Verify licensed professionals in Israel for AI agents: real-estate brokers, registered contractors, appraisers, CPAs, surveyors, private investigators — live from data.gov.il"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Oleg Sher" }]
13
+ keywords = ["mcp", "israel", "open-data", "data.gov.il", "model-context-protocol"]
14
+ dependencies = ["fastmcp>=2.0", "httpx>=0.27"]
15
+
16
+ [project.optional-dependencies]
17
+ dev = ["pytest>=8", "ruff>=0.5"]
18
+
19
+ [project.scripts]
20
+ mcp-il-professionals = "mcp_il_professionals.server:main"
21
+
22
+ [project.urls]
23
+ Repository = "https://github.com/olegsher-ds-2025/mcp-il-professionals"
24
+
25
+ [tool.hatch.build.targets.wheel]
26
+ packages = ["src/mcp_il_professionals"]
27
+
28
+ [tool.ruff]
29
+ line-length = 140
30
+ target-version = "py311"
31
+
32
+ [tool.ruff.lint]
33
+ select = ["E", "F", "I", "B", "UP"]
34
+
35
+ [tool.ruff.lint.per-file-ignores]
36
+ "tests/*" = ["E501"]
37
+
38
+ [tool.pytest.ini_options]
39
+ testpaths = ["tests"]
40
+ pythonpath = ["src"]
41
+ markers = ["live: hits data.gov.il (run with -m live)"]
42
+ addopts = "-m 'not live'"
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.olegsher-ds-2025/mcp-il-professionals",
4
+ "title": "Israel Licensed Professionals",
5
+ "description": "Verify licensed professionals in Israel for AI agents: real-estate brokers, registered contractors, ",
6
+ "repository": {
7
+ "url": "https://github.com/olegsher-ds-2025/mcp-il-professionals",
8
+ "source": "github"
9
+ },
10
+ "version": "0.1.0",
11
+ "packages": [
12
+ {
13
+ "registryType": "pypi",
14
+ "identifier": "mcp-il-professionals",
15
+ "version": "0.1.0",
16
+ "transport": {
17
+ "type": "stdio"
18
+ }
19
+ }
20
+ ]
21
+ }
@@ -0,0 +1,7 @@
1
+ """Prefect Horizon entrypoint: server.py:mcp"""
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ sys.path.insert(0, str(Path(__file__).parent / "src"))
6
+
7
+ from mcp_il_professionals.server import mcp # noqa: E402,F401
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,81 @@
1
+ """Minimal async client for the data.gov.il CKAN datastore API."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import json
6
+ from datetime import UTC, datetime
7
+ from typing import Any
8
+
9
+ import httpx
10
+
11
+ BASE_URL = "https://data.gov.il/api/3/action"
12
+ USER_AGENT = "mcp-il/0.1 (+https://github.com/olegsher-ds-2025)"
13
+ TIMEOUT = httpx.Timeout(30.0)
14
+
15
+ _DROP = {"_id", "rank", "_full_text"}
16
+
17
+
18
+ class CkanError(RuntimeError):
19
+ pass
20
+
21
+
22
+ def clean_value(v: Any) -> Any:
23
+ """data.gov.il registries encode a double quote as '~' (e.g. בע~מ -> בע"מ)."""
24
+ if isinstance(v, str):
25
+ v = v.replace("~", '"').strip()
26
+ return v or None
27
+ return v
28
+
29
+
30
+ def clean_record(rec: dict) -> dict:
31
+ return {k: clean_value(v) for k, v in rec.items() if k not in _DROP}
32
+
33
+
34
+ async def datastore_search(
35
+ resource_id: str,
36
+ *,
37
+ filters: dict | None = None,
38
+ q: str | dict | None = None,
39
+ limit: int = 10,
40
+ offset: int = 0,
41
+ fields: list[str] | None = None,
42
+ sort: str | None = None,
43
+ retries: int = 3,
44
+ ) -> dict:
45
+ """Query one datastore resource. Returns {'total': int, 'records': [cleaned dicts]}."""
46
+ params: dict[str, Any] = {"resource_id": resource_id, "limit": max(1, min(limit, 1000)), "offset": offset}
47
+ if filters:
48
+ params["filters"] = json.dumps(filters, ensure_ascii=False)
49
+ if q:
50
+ params["q"] = json.dumps(q, ensure_ascii=False) if isinstance(q, dict) else q
51
+ if fields:
52
+ params["fields"] = ",".join(fields)
53
+ if sort:
54
+ params["sort"] = sort
55
+
56
+ last_exc: Exception | None = None
57
+ async with httpx.AsyncClient(timeout=TIMEOUT, headers={"User-Agent": USER_AGENT}) as client:
58
+ for attempt in range(retries):
59
+ try:
60
+ r = await client.get(f"{BASE_URL}/datastore_search", params=params)
61
+ r.raise_for_status()
62
+ body = r.json()
63
+ if not body.get("success"):
64
+ raise CkanError(str(body.get("error")))
65
+ res = body["result"]
66
+ return {"total": res.get("total", 0), "records": [clean_record(x) for x in res.get("records", [])]}
67
+ except (httpx.HTTPError, CkanError, ValueError) as exc:
68
+ last_exc = exc
69
+ await asyncio.sleep(1.5 * (attempt + 1))
70
+ raise CkanError(f"data.gov.il query failed for {resource_id}: {last_exc}")
71
+
72
+
73
+ def meta(dataset: str | list[str]) -> dict:
74
+ """Standard provenance block added to every tool response."""
75
+ names = [dataset] if isinstance(dataset, str) else dataset
76
+ return {
77
+ "source": "data.gov.il — " + "; ".join(names),
78
+ "retrieved_at": datetime.now(UTC).isoformat(timespec="seconds"),
79
+ "attribution": "Israeli government open data (data.gov.il). Not an official service; verify critical facts "
80
+ "with the publishing authority.",
81
+ }
@@ -0,0 +1,250 @@
1
+ """mcp-il-professionals — verify licensed professionals in Israel: real-estate brokers, registered
2
+ contractors, real-estate appraisers, CPAs, vehicle appraisers, licensed surveyors and private investigators.
3
+ Live data from data.gov.il (Ministries of Justice, Construction & Housing, Transport, Survey of Israel, CPA Council)."""
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+ import os
8
+
9
+ from fastmcp import FastMCP
10
+
11
+ from .ckan import datastore_search, meta
12
+
13
+ R_BROKERS = "a0f56034-88db-4132-8803-854bcdb01ca1" # מאגר מתווכים פעילים
14
+ R_CONTRACTORS = "4eb61bd6-18cf-4e7c-9f9c-e166dfa0a2d8" # פנקס הקבלנים הרשומים
15
+ R_APPRAISERS = "8540534a-eccd-4568-a677-652d589ed172" # מאגר שמאי מקרקעין
16
+ R_CPAS = "7e86def9-0899-45f4-8942-d6fe38482b1a" # מאגר רואי החשבון
17
+ R_VEHICLE_APPRAISERS = "4a434d65-3ca2-45e5-8026-5d9819c3f95c" # שמאי רכב
18
+ R_SURVEYORS = "096e5fba-420c-4822-8172-78be54767cef" # רשימת המודדים (מפ"י)
19
+ R_INVESTIGATORS = "a9ddb445-825f-4635-a573-fe82d2a95bf0" # חוקרים פרטיים
20
+ R_INVESTIGATION_FIRMS = "94722051-b0c7-4f73-b543-c558c6c2c660" # תאגידי חוקרים פרטיים
21
+
22
+
23
+ def _broker(r):
24
+ return {"name": r.get("שם המתווך"), "license_number": r.get("מס רשיון"), "city": r.get("עיר מגורים"),
25
+ "status": "active (listed in the active brokers registry)"}
26
+
27
+
28
+ def _appraiser(r):
29
+ return {"name": r.get("שם שמאי"), "license_number": r.get("מספר רשיון"), "file_number": r.get("מספר תיק"),
30
+ "city": r.get("עיר")}
31
+
32
+
33
+ def _cpa(r):
34
+ return {"first_name": r.get("שם פרטי"), "last_name": r.get("שם משפחה"), "license_number": r.get("מספר רישיון"),
35
+ "licensed_since": r.get("תאריך קבלת רישיון"), "city": r.get("ישוב"), "status": r.get("סטטוס")}
36
+
37
+
38
+ def _vehicle_appraiser(r):
39
+ return {"first_name": r.get("shem_prati"), "last_name": r.get("shem_mishpaha"),
40
+ "license_number": r.get("mispar_rishayon"), "city": r.get("yishuv")}
41
+
42
+
43
+ def _surveyor(r):
44
+ return {"first_name": r.get("name1"), "last_name": r.get("family_name"), "license_number": r.get("number"),
45
+ "status": r.get("status"), "notes": r.get("information"), "certificate": r.get("cer_link")}
46
+
47
+
48
+ def _investigator(r):
49
+ return {"first_name": r.get("שם פרטי"), "last_name": r.get("שם משפחה"), "license_number": r.get("מספר רישיון")}
50
+
51
+
52
+ def _investigation_firm(r):
53
+ return {"firm": r.get("שם תאגיד"), "license_number": r.get("מספר רישיון")}
54
+
55
+
56
+ def _contractor(r):
57
+ return {
58
+ "name": r.get("SHEM_YESHUT"), "contractor_number": r.get("MISPAR_KABLAN"),
59
+ "company_or_id_number": r.get("MISPAR_YESHUT"), "branch": " ".join((r.get("TEUR_ANAF") or "").split()) or None,
60
+ "branch_code": r.get("KOD_ANAF"), "group": r.get("KVUTZA"), "classification": r.get("SIVUG"),
61
+ "classification_since": r.get("TARICH_SUG"), "registered_since": (r.get("TAARICH_KABLAN") or "")[:10] or None,
62
+ "recognized_for_government_work": r.get("KABLAN_MUKAR"), "city": r.get("SHEM_YISHUV"),
63
+ "address": " ".join(x for x in (r.get("SHEM_REHOV"), r.get("MISPAR_BAIT")) if x) or None,
64
+ "phone": r.get("MISPAR_TEL"), "email": r.get("EMAIL"), "qualified_manager": r.get("OVDIM"),
65
+ "notes": r.get("HEARA"),
66
+ }
67
+
68
+
69
+ # profession -> (resource, mapper, license field, dataset label)
70
+ REGISTRIES = {
71
+ "real_estate_broker": (R_BROKERS, _broker, "מס רשיון", "Ministry of Justice — licensed real-estate brokers"),
72
+ "real_estate_appraiser": (R_APPRAISERS, _appraiser, "מספר רשיון", "Ministry of Justice — real-estate appraisers"),
73
+ "cpa": (R_CPAS, _cpa, "מספר רישיון", "CPA Council — certified public accountants"),
74
+ "vehicle_appraiser": (R_VEHICLE_APPRAISERS, _vehicle_appraiser, "mispar_rishayon",
75
+ "Ministry of Transport — vehicle appraisers"),
76
+ "surveyor": (R_SURVEYORS, _surveyor, "number", "Survey of Israel — licensed surveyors"),
77
+ "private_investigator": (R_INVESTIGATORS, _investigator, "מספר רישיון",
78
+ "Ministry of Justice — private investigators"),
79
+ "investigation_firm": (R_INVESTIGATION_FIRMS, _investigation_firm, "מספר רישיון",
80
+ "Ministry of Justice — private investigation firms"),
81
+ "contractor": (R_CONTRACTORS, _contractor, "MISPAR_KABLAN", "Registrar of Contractors — registered contractors"),
82
+ }
83
+ STRING_LICENSE = {"contractor"} # stored as text in the dataset
84
+
85
+ NOT_OPEN = {
86
+ "electrician": {
87
+ "regulator": "Ministry of Labor — Electricians Licensing Unit (המחלקה לרישוי חשמלאים)",
88
+ "how_to_verify": "Ask for the licence number and ID; check it in the Ministry of Labor online electrician "
89
+ "licence search. Licence grade matters (e.g. מוסמך / ראשי / בודק).",
90
+ "info_page": "https://www.gov.il/he/departments/units/electricity-licensing-unit",
91
+ },
92
+ "engineer_or_architect": {
93
+ "regulator": "Ministry of Labor — Registrar of Engineers and Architects (פנקס המהנדסים והאדריכלים)",
94
+ "how_to_verify": "Use the Registrar's online search on gov.il; registration and licence are separate steps.",
95
+ },
96
+ "lawyer": {
97
+ "regulator": "Israel Bar Association",
98
+ "how_to_verify": "Use the Bar Association's public lawyer search.",
99
+ },
100
+ }
101
+
102
+ mcp = FastMCP(
103
+ "mcp-il-professionals",
104
+ instructions=(
105
+ "Verify Israeli licensed professionals before hiring. Professions: " + ", ".join(REGISTRIES)
106
+ + ". Use find_contractors for registered building contractors by city/branch. Electricians, engineers and "
107
+ "lawyers are not in open data — call list_professions for how to verify them. Names are Hebrew."
108
+ ),
109
+ )
110
+
111
+
112
+ @mcp.tool
113
+ async def list_professions() -> dict:
114
+ """Which professions can be verified here, and how to verify those that are not open data
115
+ (electricians, engineers/architects, lawyers)."""
116
+ return {
117
+ "available": {k: v[3] for k, v in REGISTRIES.items()},
118
+ "not_in_open_data": NOT_OPEN,
119
+ **meta("data.gov.il catalogue"),
120
+ }
121
+
122
+
123
+ @mcp.tool
124
+ async def verify_professional(profession: str, name: str = "", license_number: str = "", city: str = "",
125
+ limit: int = 20) -> dict:
126
+ """Check that a person or firm holds a licence in an Israeli professional registry.
127
+
128
+ Args:
129
+ profession: one of real_estate_broker, real_estate_appraiser, cpa, vehicle_appraiser, surveyor,
130
+ private_investigator, investigation_firm, contractor (or electrician / engineer_or_architect /
131
+ lawyer — returns how to verify, these are not open data).
132
+ name: full or partial name in Hebrew (e.g. "גריצר דורית").
133
+ license_number: licence / registration number (exact match).
134
+ city: optional city text to narrow name searches.
135
+ """
136
+ if profession in NOT_OPEN:
137
+ return {"profession": profession, "open_data": False, **NOT_OPEN[profession]}
138
+ if profession not in REGISTRIES:
139
+ raise ValueError(f"Unknown profession. Choose from: {', '.join(list(REGISTRIES) + list(NOT_OPEN))}")
140
+ rid, mapper, lic_field, label = REGISTRIES[profession]
141
+ if license_number:
142
+ digits = "".join(ch for ch in license_number if ch.isdigit())
143
+ value = digits if profession in STRING_LICENSE else int(digits)
144
+ res = await datastore_search(rid, filters={lic_field: value}, limit=20)
145
+ elif name:
146
+ q = " ".join(x for x in (name, city) if x)
147
+ res = await datastore_search(rid, q=q, limit=max(1, min(limit, 100)))
148
+ else:
149
+ raise ValueError("Provide name or license_number")
150
+ rows = [mapper(r) for r in res["records"]]
151
+ return {
152
+ "profession": profession,
153
+ "found": bool(rows),
154
+ "total": res["total"],
155
+ "results": rows,
156
+ "note": None if rows else "Not found. Check spelling (Hebrew), try the licence number, or the person may be "
157
+ "unlicensed / inactive.",
158
+ **meta(label),
159
+ }
160
+
161
+
162
+ @mcp.tool
163
+ async def find_contractors(city: str = "", branch: str = "", name: str = "", classification: str = "",
164
+ group: str = "", government_recognized_only: bool = False, limit: int = 20) -> dict:
165
+ """Registered building contractors (פנקס הקבלנים) by city, branch keyword, name and classification.
166
+
167
+ Args:
168
+ city: city in Hebrew (e.g. "מודיעין", "בת ים").
169
+ branch: work type keyword, e.g. "שיפוצים", "בניה", "חשמלאות", "אינסטלציה", "מיזוג", "איטום", "כבישים".
170
+ classification: financial-scope class "1"-"5" ("ק" = small). Higher allows larger projects.
171
+ group: registry group letter "א", "ב", "ג"...
172
+ government_recognized_only: only contractors recognized for government tenders (קבלן מוכר).
173
+ """
174
+ filters: dict = {}
175
+ if classification:
176
+ filters["SIVUG"] = classification
177
+ if group:
178
+ filters["KVUTZA"] = group
179
+ q = " ".join(x for x in (name, city, branch) if x) or None
180
+ res = await datastore_search(R_CONTRACTORS, q=q, filters=filters or None, limit=max(1, min(limit, 200)))
181
+ rows = [_contractor(r) for r in res["records"]]
182
+ if government_recognized_only:
183
+ rows = [r for r in rows if (r.get("recognized_for_government_work") or "").startswith("מוכר")]
184
+ return {
185
+ "total_matches": res["total"],
186
+ "results": rows,
187
+ "legend": {"classification": "1 = smallest scope … 5 = unlimited; ק = small works",
188
+ "group": "Registry group letter within the branch"},
189
+ **meta(REGISTRIES["contractor"][3]),
190
+ }
191
+
192
+
193
+ @mcp.tool
194
+ async def contractor_profile(contractor_number: str = "", company_number: str = "") -> dict:
195
+ """All registered branches and classifications for one contractor (by contractor number or company/ID number)."""
196
+ if contractor_number:
197
+ filters = {"MISPAR_KABLAN": "".join(c for c in contractor_number if c.isdigit())}
198
+ elif company_number:
199
+ filters = {"MISPAR_YESHUT": "".join(c for c in company_number if c.isdigit())}
200
+ else:
201
+ raise ValueError("Provide contractor_number or company_number")
202
+ res = await datastore_search(R_CONTRACTORS, filters=filters, limit=100)
203
+ rows = [_contractor(r) for r in res["records"]]
204
+ if not rows:
205
+ return {"found": False, **meta(REGISTRIES["contractor"][3])}
206
+ head = {k: rows[0][k] for k in ("name", "contractor_number", "company_or_id_number", "city", "address", "phone",
207
+ "email", "registered_since")}
208
+ branches = [{k: r[k] for k in ("branch", "group", "classification", "classification_since",
209
+ "recognized_for_government_work", "qualified_manager")} for r in rows]
210
+ return {"found": True, "contractor": head, "branches": branches, **meta(REGISTRIES["contractor"][3])}
211
+
212
+
213
+ @mcp.tool
214
+ async def search_all_registries(name: str, limit_per_registry: int = 5) -> dict:
215
+ """Search a name across every professional registry at once — useful to see all licences a person holds."""
216
+ lim = max(1, min(limit_per_registry, 20))
217
+ keys = list(REGISTRIES)
218
+ res = await asyncio.gather(*(datastore_search(REGISTRIES[k][0], q=name, limit=lim) for k in keys),
219
+ return_exceptions=True)
220
+ out, errors = {}, {}
221
+ for k, r in zip(keys, res, strict=True):
222
+ if isinstance(r, Exception):
223
+ errors[k] = str(r)
224
+ elif r["records"]:
225
+ out[k] = {"total": r["total"], "results": [REGISTRIES[k][1](x) for x in r["records"]]}
226
+ body = {"query": name, "matches": out, **meta([v[3] for v in REGISTRIES.values()])}
227
+ if errors:
228
+ body["errors"] = errors
229
+ return body
230
+
231
+
232
+ @mcp.tool
233
+ async def health() -> dict:
234
+ """Check that the server can reach data.gov.il."""
235
+ try:
236
+ res = await datastore_search(R_CONTRACTORS, limit=1)
237
+ return {"status": "ok", "registered_contractor_rows": res["total"], **meta(REGISTRIES["contractor"][3])}
238
+ except Exception as exc: # noqa: BLE001
239
+ return {"status": "error", "error": str(exc)}
240
+
241
+
242
+ def main() -> None:
243
+ if os.environ.get("MCP_TRANSPORT", "stdio") == "stdio":
244
+ mcp.run()
245
+ else:
246
+ mcp.run(transport="http", host="0.0.0.0", port=int(os.environ.get("PORT", "8000")))
247
+
248
+
249
+ if __name__ == "__main__":
250
+ main()
@@ -0,0 +1,50 @@
1
+ import asyncio
2
+
3
+ import pytest
4
+ from fastmcp import Client
5
+
6
+ from mcp_il_professionals import server
7
+
8
+ FAKE = {"total": 1, "records": [{"dummy": "value"}]}
9
+
10
+
11
+ async def _fake_search(resource_id, **kwargs):
12
+ return FAKE
13
+
14
+
15
+ def _call(tool, args):
16
+ async def run():
17
+ async with Client(server.mcp) as client:
18
+ return await client.call_tool(tool, args)
19
+
20
+ return asyncio.run(run())
21
+
22
+
23
+ def test_tools_registered():
24
+ async def run():
25
+ async with Client(server.mcp) as client:
26
+ return {t.name for t in await client.list_tools()}
27
+
28
+ names = asyncio.run(run())
29
+ assert {'verify_professional', 'contractor_profile', 'find_contractors', 'search_all_registries', 'health', 'list_professions'} <= names
30
+
31
+
32
+ def test_offline_call_has_provenance(monkeypatch):
33
+ monkeypatch.setattr(server, "datastore_search", _fake_search)
34
+ result = _call('verify_professional', {'profession': 'cpa', 'name': 'כהן'})
35
+ data = result.data if hasattr(result, "data") else result
36
+ assert "source" in data and "data.gov.il" in data["source"]
37
+
38
+
39
+ def test_clean_value():
40
+ from mcp_il_professionals.ckan import clean_value
41
+
42
+ assert clean_value("בע~מ") == 'בע"מ'
43
+ assert clean_value(" ") is None
44
+
45
+
46
+ @pytest.mark.live
47
+ def test_live_smoke():
48
+ result = _call('find_contractors', {'city': 'מודיעין', 'branch': 'שיפוצים', 'limit': 2})
49
+ data = result.data if hasattr(result, "data") else result
50
+ assert "retrieved_at" in data