rrk-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.
- rrk_mcp-0.1.0/.github/workflows/ci.yml +22 -0
- rrk_mcp-0.1.0/.github/workflows/publish.yml +51 -0
- rrk_mcp-0.1.0/.gitignore +6 -0
- rrk_mcp-0.1.0/LICENSE +21 -0
- rrk_mcp-0.1.0/PKG-INFO +103 -0
- rrk_mcp-0.1.0/README.md +75 -0
- rrk_mcp-0.1.0/pyproject.toml +56 -0
- rrk_mcp-0.1.0/server.json +32 -0
- rrk_mcp-0.1.0/src/rrk_mcp/__init__.py +5 -0
- rrk_mcp-0.1.0/src/rrk_mcp/certs/certum_dv_tls_g2_r39.pem +38 -0
- rrk_mcp-0.1.0/src/rrk_mcp/exchanges.py +139 -0
- rrk_mcp-0.1.0/src/rrk_mcp/http.py +252 -0
- rrk_mcp-0.1.0/src/rrk_mcp/laws.py +389 -0
- rrk_mcp-0.1.0/src/rrk_mcp/registry.py +16 -0
- rrk_mcp-0.1.0/src/rrk_mcp/server.py +92 -0
- rrk_mcp-0.1.0/tests/conftest.py +46 -0
- rrk_mcp-0.1.0/tests/fixtures/approvers.json +1 -0
- rrk_mcp-0.1.0/tests/fixtures/exchanges_page.html +7 -0
- rrk_mcp-0.1.0/tests/fixtures/exchanges_report.html +9 -0
- rrk_mcp-0.1.0/tests/fixtures/law_page.html +27 -0
- rrk_mcp-0.1.0/tests/fixtures/report.html +5 -0
- rrk_mcp-0.1.0/tests/fixtures/search_page.html +10 -0
- rrk_mcp-0.1.0/tests/test_live_rrk.py +51 -0
- rrk_mcp-0.1.0/tests/test_rrk.py +205 -0
- rrk_mcp-0.1.0/uv.lock +1362 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python: ["3.10", "3.11", "3.12", "3.13"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v7
|
|
16
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
17
|
+
with:
|
|
18
|
+
python-version: ${{ matrix.python }}
|
|
19
|
+
- run: uv sync --locked
|
|
20
|
+
- run: uv run ruff check .
|
|
21
|
+
- run: uv run ruff format --check .
|
|
22
|
+
- run: uv run pytest -q
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
# Tag a release (git tag v0.1.0 && git push --tags) after bumping the version in
|
|
4
|
+
# pyproject.toml and server.json.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags: ["v*"]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
pypi:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
environment: pypi
|
|
13
|
+
permissions:
|
|
14
|
+
id-token: write # PyPI trusted publishing
|
|
15
|
+
contents: write # GitHub Release
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v7
|
|
18
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
19
|
+
- name: Check versions match the tag
|
|
20
|
+
run: |
|
|
21
|
+
TAG=${GITHUB_REF_NAME#v}
|
|
22
|
+
grep -q "^version = \"$TAG\"" pyproject.toml
|
|
23
|
+
test "$(jq -r .version server.json)" = "$TAG"
|
|
24
|
+
test "$(jq -r '.packages[0].version' server.json)" = "$TAG"
|
|
25
|
+
- run: uv sync --locked && uv run pytest -q
|
|
26
|
+
- run: uv build
|
|
27
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
28
|
+
- run: gh release create "$GITHUB_REF_NAME" dist/* --generate-notes --title "$GITHUB_REF_NAME"
|
|
29
|
+
env:
|
|
30
|
+
GH_TOKEN: ${{ github.token }}
|
|
31
|
+
|
|
32
|
+
mcp-registry:
|
|
33
|
+
needs: pypi
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
permissions:
|
|
36
|
+
id-token: write # MCP Registry login via GitHub OIDC
|
|
37
|
+
contents: read
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v7
|
|
40
|
+
- name: Wait for PyPI to serve the new version
|
|
41
|
+
run: |
|
|
42
|
+
for i in $(seq 1 30); do
|
|
43
|
+
curl -sf "https://pypi.org/pypi/rrk-mcp/${GITHUB_REF_NAME#v}/json" >/dev/null && exit 0
|
|
44
|
+
sleep 10
|
|
45
|
+
done
|
|
46
|
+
exit 1
|
|
47
|
+
- name: Install mcp-publisher
|
|
48
|
+
run: |
|
|
49
|
+
curl -L "https://github.com/modelcontextprotocol/registry/releases/download/v1.8.1/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
|
|
50
|
+
- run: ./mcp-publisher login github-oidc
|
|
51
|
+
- run: ./mcp-publisher publish
|
rrk_mcp-0.1.0/.gitignore
ADDED
rrk_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sepehr Radmard
|
|
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.
|
rrk_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: rrk-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unofficial read-only MCP server for the Official Gazette of Iran (rrk.ir, روزنامه رسمی): laws, regulations and court rulings.
|
|
5
|
+
Project-URL: Homepage, https://github.com/sepehr071/rrk-mcp
|
|
6
|
+
Project-URL: Issues, https://github.com/sepehr071/rrk-mcp/issues
|
|
7
|
+
Project-URL: Website, https://mcpfarsi.ir/
|
|
8
|
+
Author-email: Sepehr Radmard <sepehrr80@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: claude,iran-law,laws,legal,mcp,model-context-protocol,official-gazette,persian,regulations,rrk,rrk-ir
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Text Processing
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: curl-cffi>=0.16.3
|
|
24
|
+
Requires-Dist: httpx-curl-cffi>=0.1.5
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Requires-Dist: mcp<3,>=2.2
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
<!-- mcp-name: io.github.sepehr071/rrk-mcp -->
|
|
30
|
+
|
|
31
|
+
<div align="center">
|
|
32
|
+
|
|
33
|
+
# rrk-mcp: Official Gazette of Iran (روزنامه رسمی) laws and rulings MCP server for AI agents
|
|
34
|
+
|
|
35
|
+
**rrk-mcp is an unofficial, read-only MCP server for the laws and regulations database of the Official Gazette of Iran (rrk.ir).**<br>
|
|
36
|
+
Its 5 tools list the newest laws, search them by title, full text, group, approver, special issue number or gazette date,<br>
|
|
37
|
+
read the full text of a law, court ruling or regulation, and list the licensed exchange offices. No API key or account is needed.
|
|
38
|
+
|
|
39
|
+
[](https://pypi.org/project/rrk-mcp/)
|
|
40
|
+
[](https://pypi.org/project/rrk-mcp/)
|
|
41
|
+
[](https://github.com/sepehr071/rrk-mcp/blob/main/LICENSE)
|
|
42
|
+
|
|
43
|
+
[For AI agents](#for-ai-agents) · [Quick start](#quick-start) · [Tools](#tools) · [Limits](#limits) · [فارسی](#فارسی)
|
|
44
|
+
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## For AI agents
|
|
50
|
+
|
|
51
|
+
- **What:** unofficial MCP server for rrk.ir, the Official Gazette: laws (قوانین), regulations (آییننامه، مصوبات), Court of Administrative Justice rulings (آرای دیوان عدالت اداری) and other texts published in the gazette since 1285 (2026 = 1405).
|
|
52
|
+
- **Install:** `uvx rrk-mcp` or `claude mcp add rrk -- uvx rrk-mcp`. Transport: stdio. Needs [uv](https://docs.astral.sh/uv/getting-started/installation/) (Python 3.10+).
|
|
53
|
+
- **Auth:** none. Optional `RRK_MCP_PROXY` (HTTP proxy URL); system proxy settings are ignored.
|
|
54
|
+
- **Access:** read-only; no confirmation is needed before calling any tool.
|
|
55
|
+
- **Tools:** 5, all prefixed `rk_`. Typical sequence: `rk_law_filters` (optional) -> `rk_search_laws` -> `rk_law_text`.
|
|
56
|
+
- **Use when:** the user asks what the gazette published lately, wants a law or ruling by topic or date, or needs the text of one.
|
|
57
|
+
- **Do not use when:** the user wants company registration notices (founding, capital change, board change, liquidation). rrk.ir does not offer those in an open search, so this server cannot look companies up.
|
|
58
|
+
- **Output:** compact JSON, Persian text. Dates are Jalali (`1405/08/09`).
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
claude mcp add rrk -- uvx rrk-mcp
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Any other client: run `uvx rrk-mcp` as a stdio MCP server (`{"command": "uvx", "args": ["rrk-mcp"]}`).
|
|
67
|
+
|
|
68
|
+
## Tools
|
|
69
|
+
|
|
70
|
+
| Tool | What it does |
|
|
71
|
+
|---|---|
|
|
72
|
+
| `rk_latest_laws` | List the newest laws, rulings and regulations in the Official Gazette, newest first |
|
|
73
|
+
| `rk_search_laws` | Search laws by title words, full-text words, law group, approver, special issue number or Jalali gazette date range |
|
|
74
|
+
| `rk_law_filters` | List the law groups (categories) and approving authorities to filter by |
|
|
75
|
+
| `rk_law_text` | Read one law's approver, gazette number and date, and full text (long texts in parts) |
|
|
76
|
+
| `rk_exchange_offices` | List the licensed currency exchange offices (location, phone, address) by province and city |
|
|
77
|
+
|
|
78
|
+
All 5 tools are annotated `readOnlyHint: true`.
|
|
79
|
+
|
|
80
|
+
## Limits
|
|
81
|
+
|
|
82
|
+
- Laws only. Company notices (آگهیهای ثبتی) sit behind rrk.ir's paid data service and login, so they are left out.
|
|
83
|
+
- The portal is slow (a full-text search can take 10 seconds) and shares one session; the server keeps one session and reuses it.
|
|
84
|
+
- `rk_law_text` works with any code, but a code the session has not listed is located by paging the list (10-60 s, and codes below about 10,000 may not be found). Majlis debates (group 64) have a title only; the debate text is a PDF behind login.
|
|
85
|
+
- Gazette PDFs and the shop need login or payment, and are left out.
|
|
86
|
+
- Persian ی and ک are converted to the Arabic ي and ك the portal stores; you can type either.
|
|
87
|
+
|
|
88
|
+
## Development
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
uv sync
|
|
92
|
+
uv run ruff check . && uv run ruff format --check .
|
|
93
|
+
uv run python -m pytest -q
|
|
94
|
+
uv run python -m pytest -m live -q
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## فارسی
|
|
98
|
+
|
|
99
|
+
`rrk-mcp` یک سرور MCP غیررسمی و فقطخواندنی برای پایگاه قوانین و مقررات روزنامه رسمی (rrk.ir) است: فهرست جدیدترین قوانین، جستجو بر اساس عنوان، متن، شماره ویژهنامه و تاریخ روزنامه، فیلتر بر اساس گروه قوانین و مرجع تصویب، خواندن متن کامل قانون یا رأی، و فهرست صرافیهای مجاز بانک مرکزی. جستجوی آگهیهای ثبتی شرکتها در سایت بهصورت باز وجود ندارد و پشتیبانی نمیشود.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
Made by Sepehr Radmard (سپهر رادمرد) · [github.com/sepehr071](https://github.com/sepehr071) · [linkedin.com/in/sepehr-radmard](https://linkedin.com/in/sepehr-radmard)
|
rrk_mcp-0.1.0/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<!-- mcp-name: io.github.sepehr071/rrk-mcp -->
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
# rrk-mcp: Official Gazette of Iran (روزنامه رسمی) laws and rulings MCP server for AI agents
|
|
6
|
+
|
|
7
|
+
**rrk-mcp is an unofficial, read-only MCP server for the laws and regulations database of the Official Gazette of Iran (rrk.ir).**<br>
|
|
8
|
+
Its 5 tools list the newest laws, search them by title, full text, group, approver, special issue number or gazette date,<br>
|
|
9
|
+
read the full text of a law, court ruling or regulation, and list the licensed exchange offices. No API key or account is needed.
|
|
10
|
+
|
|
11
|
+
[](https://pypi.org/project/rrk-mcp/)
|
|
12
|
+
[](https://pypi.org/project/rrk-mcp/)
|
|
13
|
+
[](https://github.com/sepehr071/rrk-mcp/blob/main/LICENSE)
|
|
14
|
+
|
|
15
|
+
[For AI agents](#for-ai-agents) · [Quick start](#quick-start) · [Tools](#tools) · [Limits](#limits) · [فارسی](#فارسی)
|
|
16
|
+
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## For AI agents
|
|
22
|
+
|
|
23
|
+
- **What:** unofficial MCP server for rrk.ir, the Official Gazette: laws (قوانین), regulations (آییننامه، مصوبات), Court of Administrative Justice rulings (آرای دیوان عدالت اداری) and other texts published in the gazette since 1285 (2026 = 1405).
|
|
24
|
+
- **Install:** `uvx rrk-mcp` or `claude mcp add rrk -- uvx rrk-mcp`. Transport: stdio. Needs [uv](https://docs.astral.sh/uv/getting-started/installation/) (Python 3.10+).
|
|
25
|
+
- **Auth:** none. Optional `RRK_MCP_PROXY` (HTTP proxy URL); system proxy settings are ignored.
|
|
26
|
+
- **Access:** read-only; no confirmation is needed before calling any tool.
|
|
27
|
+
- **Tools:** 5, all prefixed `rk_`. Typical sequence: `rk_law_filters` (optional) -> `rk_search_laws` -> `rk_law_text`.
|
|
28
|
+
- **Use when:** the user asks what the gazette published lately, wants a law or ruling by topic or date, or needs the text of one.
|
|
29
|
+
- **Do not use when:** the user wants company registration notices (founding, capital change, board change, liquidation). rrk.ir does not offer those in an open search, so this server cannot look companies up.
|
|
30
|
+
- **Output:** compact JSON, Persian text. Dates are Jalali (`1405/08/09`).
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
claude mcp add rrk -- uvx rrk-mcp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Any other client: run `uvx rrk-mcp` as a stdio MCP server (`{"command": "uvx", "args": ["rrk-mcp"]}`).
|
|
39
|
+
|
|
40
|
+
## Tools
|
|
41
|
+
|
|
42
|
+
| Tool | What it does |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `rk_latest_laws` | List the newest laws, rulings and regulations in the Official Gazette, newest first |
|
|
45
|
+
| `rk_search_laws` | Search laws by title words, full-text words, law group, approver, special issue number or Jalali gazette date range |
|
|
46
|
+
| `rk_law_filters` | List the law groups (categories) and approving authorities to filter by |
|
|
47
|
+
| `rk_law_text` | Read one law's approver, gazette number and date, and full text (long texts in parts) |
|
|
48
|
+
| `rk_exchange_offices` | List the licensed currency exchange offices (location, phone, address) by province and city |
|
|
49
|
+
|
|
50
|
+
All 5 tools are annotated `readOnlyHint: true`.
|
|
51
|
+
|
|
52
|
+
## Limits
|
|
53
|
+
|
|
54
|
+
- Laws only. Company notices (آگهیهای ثبتی) sit behind rrk.ir's paid data service and login, so they are left out.
|
|
55
|
+
- The portal is slow (a full-text search can take 10 seconds) and shares one session; the server keeps one session and reuses it.
|
|
56
|
+
- `rk_law_text` works with any code, but a code the session has not listed is located by paging the list (10-60 s, and codes below about 10,000 may not be found). Majlis debates (group 64) have a title only; the debate text is a PDF behind login.
|
|
57
|
+
- Gazette PDFs and the shop need login or payment, and are left out.
|
|
58
|
+
- Persian ی and ک are converted to the Arabic ي and ك the portal stores; you can type either.
|
|
59
|
+
|
|
60
|
+
## Development
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
uv sync
|
|
64
|
+
uv run ruff check . && uv run ruff format --check .
|
|
65
|
+
uv run python -m pytest -q
|
|
66
|
+
uv run python -m pytest -m live -q
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## فارسی
|
|
70
|
+
|
|
71
|
+
`rrk-mcp` یک سرور MCP غیررسمی و فقطخواندنی برای پایگاه قوانین و مقررات روزنامه رسمی (rrk.ir) است: فهرست جدیدترین قوانین، جستجو بر اساس عنوان، متن، شماره ویژهنامه و تاریخ روزنامه، فیلتر بر اساس گروه قوانین و مرجع تصویب، خواندن متن کامل قانون یا رأی، و فهرست صرافیهای مجاز بانک مرکزی. جستجوی آگهیهای ثبتی شرکتها در سایت بهصورت باز وجود ندارد و پشتیبانی نمیشود.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
Made by Sepehr Radmard (سپهر رادمرد) · [github.com/sepehr071](https://github.com/sepehr071) · [linkedin.com/in/sepehr-radmard](https://linkedin.com/in/sepehr-radmard)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "rrk-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Unofficial read-only MCP server for the Official Gazette of Iran (rrk.ir, روزنامه رسمی): laws, regulations and court rulings."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "Sepehr Radmard", email = "sepehrr80@gmail.com" }]
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
keywords = ["mcp", "model-context-protocol", "rrk", "rrk-ir", "official-gazette", "iran-law", "laws", "regulations", "legal", "claude", "persian"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
21
|
+
"Topic :: Text Processing",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"mcp>=2.2,<3",
|
|
25
|
+
"httpx>=0.27",
|
|
26
|
+
"httpx-curl-cffi>=0.1.5",
|
|
27
|
+
"curl-cffi>=0.16.3",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/sepehr071/rrk-mcp"
|
|
32
|
+
Issues = "https://github.com/sepehr071/rrk-mcp/issues"
|
|
33
|
+
Website = "https://mcpfarsi.ir/"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
rrk-mcp = "rrk_mcp.server:main"
|
|
37
|
+
|
|
38
|
+
[dependency-groups]
|
|
39
|
+
dev = ["pytest>=8", "ruff>=0.6"]
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["hatchling"]
|
|
43
|
+
build-backend = "hatchling.build"
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
testpaths = ["tests"]
|
|
47
|
+
addopts = "-m 'not live'"
|
|
48
|
+
markers = ["live: calls the real rrk.ir portal (run with -m live)"]
|
|
49
|
+
|
|
50
|
+
[tool.ruff]
|
|
51
|
+
line-length = 120
|
|
52
|
+
target-version = "py310"
|
|
53
|
+
|
|
54
|
+
[tool.ruff.lint]
|
|
55
|
+
select = ["E", "F", "I", "B", "UP"]
|
|
56
|
+
ignore = ["E501"] # long Field(description=...) strings read better on one line
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.sepehr071/rrk-mcp",
|
|
4
|
+
"title": "Official Gazette of Iran",
|
|
5
|
+
"description": "Read-only Official Gazette of Iran (rrk.ir): search laws and rulings, read their text.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/sepehr071/rrk-mcp",
|
|
8
|
+
"source": "github"
|
|
9
|
+
},
|
|
10
|
+
"websiteUrl": "https://github.com/sepehr071/rrk-mcp",
|
|
11
|
+
"version": "0.1.0",
|
|
12
|
+
"packages": [
|
|
13
|
+
{
|
|
14
|
+
"registryType": "pypi",
|
|
15
|
+
"registryBaseUrl": "https://pypi.org",
|
|
16
|
+
"identifier": "rrk-mcp",
|
|
17
|
+
"version": "0.1.0",
|
|
18
|
+
"runtimeHint": "uvx",
|
|
19
|
+
"transport": {
|
|
20
|
+
"type": "stdio"
|
|
21
|
+
},
|
|
22
|
+
"environmentVariables": [
|
|
23
|
+
{
|
|
24
|
+
"name": "RRK_MCP_PROXY",
|
|
25
|
+
"description": "Optional HTTP proxy URL for the rrk.ir requests; system proxy settings are ignored.",
|
|
26
|
+
"isRequired": false,
|
|
27
|
+
"isSecret": false
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIGnTCCBIWgAwIBAgIRAKgt2eXcr98TIF5wBD5rlagwDQYJKoZIhvcNAQENBQAw
|
|
3
|
+
ejELMAkGA1UEBhMCUEwxITAfBgNVBAoTGEFzc2VjbyBEYXRhIFN5c3RlbXMgUy5B
|
|
4
|
+
LjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MR8wHQYD
|
|
5
|
+
VQQDExZDZXJ0dW0gVHJ1c3RlZCBSb290IENBMB4XDTI0MDYxODA3NDEyMloXDTM5
|
|
6
|
+
MDYwNTA3NDEyMlowUjELMAkGA1UEBhMCUEwxITAfBgNVBAoMGEFzc2VjbyBEYXRh
|
|
7
|
+
IFN5c3RlbXMgUy5BLjEgMB4GA1UEAwwXQ2VydHVtIERWIFRMUyBHMiBSMzkgQ0Ew
|
|
8
|
+
ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCo52NXEXWoO2w6zQeBtNer
|
|
9
|
+
d5ahhe8RgM8XVpwGEFoovjHc4K+Cp3auUWVlt7/ARthJoxOttF+jaSrlSve9mWnm
|
|
10
|
+
TJOo1QLuoOTWuZ9XUkMjDG1ztTbFsgRqQyOtZsDqniHD79wqD49DQW4geVslp9/L
|
|
11
|
+
iTQKUpPawtAwpBeoaRXL8RJ8xjNA+2bEr6vesz2MEvvhpWBSWNAIR5O5YbiLztQ9
|
|
12
|
+
KdOuBYS0CW59ptuCjg3AuLcp8aOjk9z/kJc8xKkO48hLTp+HpdHkuI+iFWZn0aCL
|
|
13
|
+
lM/ngpdoBw+NGs6TMC8B6BcK7y/zl8FsNC4gE86Kfd8J9zWhCA7umHnBXCSYCKRx
|
|
14
|
+
H5o7DtoGiWXvcRKYpGtWt9czdUa1edSk5mTrwZGEXLAkX1ECiAq4GS5vEGjrEQ1u
|
|
15
|
+
x8mag2LDh7ZnXdcyzkKZKGsx7uExe3Nx5gWWZMXFrZ5v+uxynKogHUY2vdIMB3dn
|
|
16
|
+
9qRYwpzvn3msfBbkRTAcS9eis1AY0Xxqlt3aXkVyqfKhdJxOPpzATM+Ve4jZSd1n
|
|
17
|
+
LzEj+kFuHnv2jyOY3Vb35n3EmW8yAwG1OWX/QnemMA5s2fZ+ZydHOTG4DkwXnaTr
|
|
18
|
+
R/vUhM+FNywNUlvzYjcM6zt3Ysf9M1hK5PjUEKzsPf5BrIp0fs1zhlVC+cgBN2+J
|
|
19
|
+
PtYwxP1nNpxwBgtIPoTk6wIDAQABo4IBRDCCAUAwcQYIKwYBBQUHAQEEZTBjMDcG
|
|
20
|
+
CCsGAQUFBzAChitodHRwOi8vc3ViY2EucmVwb3NpdG9yeS5jZXJ0dW0ucGwvY3Ry
|
|
21
|
+
Y2EuY2VyMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1jZXJ0dW0uY29t
|
|
22
|
+
MB8GA1UdIwQYMBaAFIz7HHW8AtOfTi5I2flgVKrEs0/6MBIGA1UdEwEB/wQIMAYB
|
|
23
|
+
Af8CAQAwNQYDVR0fBC4wLDAqoCigJoYkaHR0cDovL3N1YmNhLmNybC5jZXJ0dW0u
|
|
24
|
+
cGwvY3RyY2EuY3JsMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATAOBgNV
|
|
25
|
+
HQ8BAf8EBAMCAQYwEQYDVR0gBAowCDAGBgRVHSAAMB0GA1UdDgQWBBQzqHe3AThv
|
|
26
|
+
Xx8kJKIebBTjbiID3zANBgkqhkiG9w0BAQ0FAAOCAgEACDQ1ggBelZZ/NYs1nFKW
|
|
27
|
+
HnDrA8Y4pv0lvxLzSAC4ejGavMXqPTXHA+DEh9kHNd8tVlo24+6YN96Gspb1kMXR
|
|
28
|
+
uuql23/6R6Fpqg49dkQ1/DobsWvAHoYeZvsaAgaKRD3bvsAcB0JBhyBVT/88S9gu
|
|
29
|
+
DnS5YKMldiLMkVW1Noskd4dHEJ2mkJcVzJIJ0Y4johA1lC1JnZMjkB8ZTNIblkgJ
|
|
30
|
+
K6PqlhYkeMOkx+XbmUuUgh29T0sPne7/V6PHnbEJIxUs40+iLCF0HrdqZypjvWQq
|
|
31
|
+
pSmHRHI3UWVERDeERca0uJ3I+a5ER9vUL9u5ilGG4afyx7QwzitBG+1rU3nRsHyZ
|
|
32
|
+
g6osILL/MWc0AbWJMyKzQ9Guj+uwq47h6BC9BsWF34pJeDC8EuN3HNxPlSWSII9l
|
|
33
|
+
Omwtipvq0EL1iocJhXdlsG+jIUVs/Sl/Um9JiZV+h/MoytnrPrWMIj+0zz6BdaPP
|
|
34
|
+
2sT6wcLzpnwYcE9FWSbQrzNpL283EOUkObjc8AIxICzPHGusF0IqsO+sj9XzvLTh
|
|
35
|
+
TjKfFlzx4NR8gbK7m8sXq6cgP4UAtyvDswebFIRQiuhjqOT9G7+56+4zC0RaEZx/
|
|
36
|
+
LwoFE+ObVXxX674szQvIc+7WPCooVsUbwZIikzJqZb4gJQ1OQx23CgyyYlsPHIDN
|
|
37
|
+
8FpPkganuCwy++7umTkM7+Q=
|
|
38
|
+
-----END CERTIFICATE-----
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Tool for the list of licensed currency exchange offices that the Official Gazette publishes (rrk.ir, page 176).
|
|
2
|
+
|
|
3
|
+
Same APEX mechanics as laws.py: load the page once for a session, then post the classic report's refresh call.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import html
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
from typing import Annotated, Any
|
|
13
|
+
|
|
14
|
+
from pydantic import Field
|
|
15
|
+
|
|
16
|
+
from . import http
|
|
17
|
+
from .http import ApiError
|
|
18
|
+
from .laws import AJAX_URL, _field, _strip
|
|
19
|
+
from .registry import tool
|
|
20
|
+
|
|
21
|
+
PAGE_URL = f"{http.SITE}/ords/r/rrs/rrs-front/r-exchanges"
|
|
22
|
+
PER_PAGE = 50 # the report's fixed page size
|
|
23
|
+
COLUMNS = ("STATENAME", "CITYNAME", "REGION", "PHONENUMBER", "ZIPCODE", "ADDRESS")
|
|
24
|
+
|
|
25
|
+
_state: dict[str, Any] = {}
|
|
26
|
+
_lock: asyncio.Lock | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _SessionLost(ApiError):
|
|
30
|
+
"""The portal answered with something other than the report (expired session); retried once with a new one."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def reset() -> None:
|
|
34
|
+
_state.clear()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def _session(force: bool = False) -> dict[str, Any]:
|
|
38
|
+
global _lock
|
|
39
|
+
_lock = _lock or asyncio.Lock()
|
|
40
|
+
async with _lock:
|
|
41
|
+
if force or not _state:
|
|
42
|
+
h = await http.fetch(PAGE_URL, cache=False)
|
|
43
|
+
sel = re.search(r'<select[^>]*id="P176_PROVINCE".*?</select>', h, re.S)
|
|
44
|
+
_state.clear()
|
|
45
|
+
_state.update(
|
|
46
|
+
instance=_field(h, r'name="p_instance" value="(\d+)"'),
|
|
47
|
+
salt=_field(h, r'value="(\d+)" id="pSalt"'),
|
|
48
|
+
protected=_field(h, r'id="pPageItemsProtected" value="([^"]+)"'),
|
|
49
|
+
region=_field(h, r'apex\.widget\.report\.init\("R(\d+)"'),
|
|
50
|
+
request=_field(h, r'apex\.widget\.report\.init\("R\d+","([^"]+)"').replace("\\u002F", "/"),
|
|
51
|
+
provinces=[html.unescape(v) for v in re.findall(r'<option value="([^"]+)"', sel[0])] if sel else [],
|
|
52
|
+
)
|
|
53
|
+
return dict(_state)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _parse(out: str, page: int) -> dict[str, Any]:
|
|
57
|
+
if "t-Report" not in out:
|
|
58
|
+
raise _SessionLost("rrk.ir returned no exchange list (session problem). Try again in a minute.")
|
|
59
|
+
m = re.search(r"t-Report-paginationText\">[^<]*?(\d+)\s*-\s*(\d+)\s*\S+\s*([\d,]+)", out)
|
|
60
|
+
total = int(m[3].replace(",", "")) if m else 0
|
|
61
|
+
offices = []
|
|
62
|
+
for row in re.findall(r"<tr>(<td class=\"t-Report-cell\".*?)</tr>", out, re.S):
|
|
63
|
+
cells = dict(re.findall(r'headers="([A-Z]+)"[^>]*>(.*?)</td>', row, re.S))
|
|
64
|
+
offices.append(
|
|
65
|
+
{
|
|
66
|
+
"province": _strip(cells.get("STATENAME", "")),
|
|
67
|
+
"city": _strip(cells.get("CITYNAME", "")),
|
|
68
|
+
"district": _strip(cells.get("REGION", "")),
|
|
69
|
+
"phone": _strip(cells.get("PHONENUMBER", "")),
|
|
70
|
+
"postal_code": _strip(cells.get("ZIPCODE", "")),
|
|
71
|
+
"address": _strip(cells.get("ADDRESS", "")),
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
return {"total": total, "page": page, "more": page * PER_PAGE < total, "offices": offices}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
_PERSIAN = str.maketrans({"ي": "ی", "ك": "ک", "ى": "ی"}) # the province/city lists use Persian ی and ک
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@tool("Licensed exchange offices")
|
|
81
|
+
async def rk_exchange_offices(
|
|
82
|
+
province: Annotated[str, Field(description="Province name as on the site, e.g. 'تهران', 'قم'. Empty = all.")] = "",
|
|
83
|
+
city: Annotated[
|
|
84
|
+
str, Field(description="City name as on the site, e.g. 'قم', 'سلفچگان'. Empty = whole province.")
|
|
85
|
+
] = "",
|
|
86
|
+
page: Annotated[int, Field(description="Page number, 50 offices per page.", ge=1, le=200)] = 1,
|
|
87
|
+
) -> dict[str, Any]:
|
|
88
|
+
"""List the licensed currency exchange offices (صرافی مجاز) the Central Bank published in the Official Gazette (rrk.ir).
|
|
89
|
+
|
|
90
|
+
Use to see where licensed exchange offices are or to find one's address and phone. Filter by `province` and
|
|
91
|
+
`city` (Persian names; an unknown province is an error that lists the valid ones). Returns `total`, `more`
|
|
92
|
+
and up to 50 `offices` with province, city, district, phone, postal_code and address. Office names are not
|
|
93
|
+
published, only the location.
|
|
94
|
+
"""
|
|
95
|
+
province, city = province.translate(_PERSIAN).strip(), city.translate(_PERSIAN).strip()
|
|
96
|
+
try:
|
|
97
|
+
return await _list(province, city, page, False)
|
|
98
|
+
except _SessionLost:
|
|
99
|
+
return await _list(province, city, page, True)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def _list(province: str, city: str, page: int, force: bool) -> dict[str, Any]:
|
|
103
|
+
s = await _session(force)
|
|
104
|
+
if province and province not in s["provinces"]:
|
|
105
|
+
raise ApiError(f"Unknown province {province!r}. Valid: " + ", ".join(s["provinces"]))
|
|
106
|
+
if city and not province:
|
|
107
|
+
raise ApiError("Give the province together with the city.")
|
|
108
|
+
form = {
|
|
109
|
+
"p_flow_id": "200",
|
|
110
|
+
"p_flow_step_id": "176",
|
|
111
|
+
"p_instance": s["instance"],
|
|
112
|
+
"p_debug": "",
|
|
113
|
+
"p_request": "PLUGIN=" + s["request"],
|
|
114
|
+
"x01": s["region"],
|
|
115
|
+
"p_widget_action": "reset",
|
|
116
|
+
"p_json": json.dumps(
|
|
117
|
+
{
|
|
118
|
+
"pageItems": {
|
|
119
|
+
"itemsToSubmit": [{"n": "P176_PROVINCE", "v": province}, {"n": "P176_CITY", "v": city}],
|
|
120
|
+
"protected": s["protected"],
|
|
121
|
+
"rowVersion": "",
|
|
122
|
+
"formRegionChecksums": [],
|
|
123
|
+
},
|
|
124
|
+
"salt": s["salt"],
|
|
125
|
+
}
|
|
126
|
+
),
|
|
127
|
+
}
|
|
128
|
+
if page > 1:
|
|
129
|
+
form |= {
|
|
130
|
+
"p_widget_action": "paginate",
|
|
131
|
+
"p_pg_min_row": str((page - 1) * PER_PAGE + 1),
|
|
132
|
+
"p_pg_max_rows": str(PER_PAGE),
|
|
133
|
+
"p_pg_rows_fetched": str(PER_PAGE),
|
|
134
|
+
}
|
|
135
|
+
out = await http.fetch(f"{AJAX_URL}?p_context=rrs-front/r-exchanges/{s['instance']}", data=form)
|
|
136
|
+
r = _parse(out, page)
|
|
137
|
+
if city and not r["total"]:
|
|
138
|
+
r["note"] = f"No office in city {city!r}; use the city name exactly as the site writes it."
|
|
139
|
+
return r
|