ifb-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.
Files changed (53) hide show
  1. ifb_mcp-0.1.0/.github/workflows/ci.yml +22 -0
  2. ifb_mcp-0.1.0/.github/workflows/publish.yml +51 -0
  3. ifb_mcp-0.1.0/.gitignore +6 -0
  4. ifb_mcp-0.1.0/LICENSE +21 -0
  5. ifb_mcp-0.1.0/PKG-INFO +144 -0
  6. ifb_mcp-0.1.0/README.md +116 -0
  7. ifb_mcp-0.1.0/pyproject.toml +56 -0
  8. ifb_mcp-0.1.0/server.json +32 -0
  9. ifb_mcp-0.1.0/src/ifb_mcp/__init__.py +5 -0
  10. ifb_mcp-0.1.0/src/ifb_mcp/grid.py +113 -0
  11. ifb_mcp-0.1.0/src/ifb_mcp/http.py +351 -0
  12. ifb_mcp-0.1.0/src/ifb_mcp/ifb.py +1124 -0
  13. ifb_mcp-0.1.0/src/ifb_mcp/registry.py +16 -0
  14. ifb_mcp-0.1.0/src/ifb_mcp/server.py +95 -0
  15. ifb_mcp-0.1.0/tests/conftest.py +80 -0
  16. ifb_mcp-0.1.0/tests/fixtures/announcements.html +40 -0
  17. ifb_mcp-0.1.0/tests/fixtures/archive.json +1 -0
  18. ifb_mcp-0.1.0/tests/fixtures/article.html +18 -0
  19. ifb_mcp-0.1.0/tests/fixtures/capital_increases.html +1110 -0
  20. ifb_mcp-0.1.0/tests/fixtures/flows.html +22 -0
  21. ifb_mcp-0.1.0/tests/fixtures/halted.html +1007 -0
  22. ifb_mcp-0.1.0/tests/fixtures/history.json +1 -0
  23. ifb_mcp-0.1.0/tests/fixtures/index.json +1 -0
  24. ifb_mcp-0.1.0/tests/fixtures/index_history.json +1 -0
  25. ifb_mcp-0.1.0/tests/fixtures/instrument.html +376 -0
  26. ifb_mcp-0.1.0/tests/fixtures/instrument_pb2.html +103 -0
  27. ifb_mcp-0.1.0/tests/fixtures/ipo.html +78 -0
  28. ifb_mcp-0.1.0/tests/fixtures/ipostatus.html +114 -0
  29. ifb_mcp-0.1.0/tests/fixtures/issuer_status.html +843 -0
  30. ifb_mcp-0.1.0/tests/fixtures/market_transfers.html +794 -0
  31. ifb_mcp-0.1.0/tests/fixtures/messages.html +18 -0
  32. ifb_mcp-0.1.0/tests/fixtures/moved_to_tehran.html +790 -0
  33. ifb_mcp-0.1.0/tests/fixtures/nelson_gov.json +1 -0
  34. ifb_mcp-0.1.0/tests/fixtures/nelson_non.json +1 -0
  35. ifb_mcp-0.1.0/tests/fixtures/nelson_tb.json +1 -0
  36. ifb_mcp-0.1.0/tests/fixtures/new_listings.html +798 -0
  37. ifb_mcp-0.1.0/tests/fixtures/news.html +33 -0
  38. ifb_mcp-0.1.0/tests/fixtures/quote.json +1 -0
  39. ifb_mcp-0.1.0/tests/fixtures/quote_bond.json +1 -0
  40. ifb_mcp-0.1.0/tests/fixtures/quote_bond_mfi.json +1 -0
  41. ifb_mcp-0.1.0/tests/fixtures/search.json +1 -0
  42. ifb_mcp-0.1.0/tests/fixtures/share_page.html +328 -0
  43. ifb_mcp-0.1.0/tests/fixtures/signalr_connect.json +1 -0
  44. ifb_mcp-0.1.0/tests/fixtures/signalr_poll.json +1 -0
  45. ifb_mcp-0.1.0/tests/fixtures/sukuk_history.json +1 -0
  46. ifb_mcp-0.1.0/tests/fixtures/sukuk_index.json +1 -0
  47. ifb_mcp-0.1.0/tests/fixtures/summary.html +3 -0
  48. ifb_mcp-0.1.0/tests/fixtures/underwritings.html +81 -0
  49. ifb_mcp-0.1.0/tests/fixtures/ytm.html +986 -0
  50. ifb_mcp-0.1.0/tests/fixtures/ytm_history.json +1 -0
  51. ifb_mcp-0.1.0/tests/test_ifb.py +430 -0
  52. ifb_mcp-0.1.0/tests/test_live_ifb.py +109 -0
  53. ifb_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/ifb-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
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ .pytest_cache/
6
+ .ruff_cache/
ifb_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.
ifb_mcp-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.5
2
+ Name: ifb-mcp
3
+ Version: 0.1.0
4
+ Summary: Unofficial read-only MCP server for Iran Fara Bourse (ifb.ir): bond and sukuk yields, treasury bills, IPOs, announcements, market stats.
5
+ Project-URL: Homepage, https://github.com/sepehr071/ifb-mcp
6
+ Project-URL: Issues, https://github.com/sepehr071/ifb-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: bonds,bourse,claude,fara-bourse,farabourse,ifb,ipo,iran,iran-fara-bourse,mcp,model-context-protocol,otc,persian,sukuk,treasury-bills,ytm
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 :: Office/Business :: Financial :: Investment
21
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
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/ifb-mcp -->
30
+
31
+ <div align="center">
32
+
33
+ # ifb-mcp: Iran Fara Bourse (فرابورس ایران) bonds, sukuk and IPO MCP server for AI agents
34
+
35
+ **ifb-mcp is an unofficial, read-only MCP server that gives AI agents the public data of Iran Fara Bourse (ifb.ir), Iran's over-the-counter exchange.**<br>
36
+ Its 16 tools rank bonds, sukuk and treasury bills (اخزا) by yield to maturity, explain a paper's coupons and issuer,<br>
37
+ quote prices and order books, list IPOs and new offerings, search exchange notices and news, and summarise the market.<br>
38
+ No API key or account is needed. It works in Claude, Cursor, Copilot and any other MCP client.
39
+
40
+ [![PyPI](https://img.shields.io/pypi/v/ifb-mcp?color=2563eb)](https://pypi.org/project/ifb-mcp/)
41
+ [![Python](https://img.shields.io/pypi/pyversions/ifb-mcp)](https://pypi.org/project/ifb-mcp/)
42
+ [![License: MIT](https://img.shields.io/badge/license-MIT-16a34a)](https://github.com/sepehr071/ifb-mcp/blob/main/LICENSE)
43
+
44
+ [For AI agents](#for-ai-agents) · [Quick start](#quick-start) · [What it can do](#what-it-can-do) · [Tools](#tools) · [FAQ](#faq) · [فارسی](#فارسی)
45
+
46
+ </div>
47
+
48
+ ---
49
+
50
+ ## For AI agents
51
+
52
+ - **What:** unofficial MCP server (model context protocol) for ifb.ir: yield to maturity of every treasury bill, sukuk and bond, instrument details with coupon schedules, live quotes and order books, price and yield history, IPOs (finished and upcoming), underwritings, exchange announcements, news and market messages, market summary, the live market watch with top movers and most traded, index history, halted symbols, capital increases and board transfers, yield curves and who trades Islamic securities.
53
+ - **Install:** `uvx ifb-mcp` or `claude mcp add ifb -- uvx ifb-mcp`. Transport: stdio. Needs [uv](https://docs.astral.sh/uv/getting-started/installation/) (Python 3.10+).
54
+ - **Auth:** none. Optional `IFB_MCP_PROXY` (HTTP proxy URL, or a comma-separated list); system proxy settings are ignored.
55
+ - **Access:** read-only. It cannot trade, log in or change anything, so no confirmation is needed before calling any tool.
56
+ - **Tools:** 16, all prefixed `ifb_`. Typical sequences: `ifb_yields` -> `ifb_instrument` / `ifb_quote`; `ifb_search` -> `ifb_history`; `ifb_ipos(stage="pipeline")`; `ifb_announcements` -> `ifb_article`.
57
+ - **Use when:** the user asks which bond, sukuk or treasury bill yields most or matures when, what a paper pays and when, a Fara Bourse price or order book, what IPOs are coming or happened, why a symbol is halted, or how the market did.
58
+ - **Do not use when:** the question is about the Tehran Stock Exchange (tse.ir), cryptocurrencies or gold; or the user wants to place an order.
59
+ - **Output:** compact JSON. Prices in rial, yields in percent a year, dates Jalali (`1405/08/09`). Texts are the exchange's own (Persian). The site is reachable from Iranian networks only.
60
+ - **Machine-readable:** [`server.json`](https://github.com/sepehr071/ifb-mcp/blob/main/server.json).
61
+
62
+ ## Why
63
+
64
+ On ifb.ir, "which treasury bill maturing before next spring pays the best yield?" means opening the yield page,
65
+ scrolling 1,000 rows and sorting by hand. With `ifb-mcp` an agent answers it in one call:
66
+
67
+ > **You:** Best yield among treasury bills maturing before 1405/12/01?
68
+ >
69
+ > **Agent:** *calls* `ifb_yields(kind="treasury", matures_before="1405/12/01", limit=3)`
70
+
71
+ <sub>Data is the exchange's, as shown on its website; yields are computed by Fara Bourse from the last trade. This is information, not investment advice.</sub>
72
+
73
+ ## What it can do
74
+
75
+ - **Rank fixed income by yield**: treasury bills, sukuk, participation papers and other bonds with last price, maturity and YTM, filtered by yield, maturity and symbol
76
+ - **Explain a paper**: coupon rate, maturity, issuer, guarantor, underwriter, market maker, full coupon schedule and the next payment
77
+ - **Quote and chart**: live price, change, volume, order book with the yield of each bid and offer, and daily history with YTM
78
+ - **Follow offerings**: finished IPOs with price and volume (filter by market and pricing method), companies being introduced, underwritings
79
+ - **Read the exchange**: search official notices, news and market messages (halts, openings) by keyword and date, and read an article
80
+ - **Watch the live market**: every symbol's price, change and best bid/ask ranked by change or value, per board; halted symbols with reasons; index history since 2009; capital increases and board transfers
81
+ - **See the market**: index, turnover, bond indices, each board's value and top movers, the fitted yield curves and who buys and sells Islamic securities
82
+ - **Read-only by design**: no login, no orders
83
+
84
+ ## Quick start
85
+
86
+ You need [uv](https://docs.astral.sh/uv/getting-started/installation/). No API key or account. ifb.ir answers Iranian
87
+ networks; from abroad set `IFB_MCP_PROXY` to an Iranian proxy.
88
+
89
+ ```bash
90
+ claude mcp add ifb -- uvx ifb-mcp
91
+ ```
92
+
93
+ Claude Desktop, Cursor and others:
94
+
95
+ ```json
96
+ {
97
+ "mcpServers": {
98
+ "ifb": { "command": "uvx", "args": ["ifb-mcp"] }
99
+ }
100
+ }
101
+ ```
102
+
103
+ ## Tools
104
+
105
+ | Tool | Use it to |
106
+ |---|---|
107
+ | `ifb_search` | turn a symbol or name into an instrument id |
108
+ | `ifb_yields` | rank bonds, sukuk and treasury bills by YTM with maturity and price filters |
109
+ | `ifb_instrument` | read coupon rate, maturity, issuer, guarantor and the coupon schedule |
110
+ | `ifb_quote` | get the live price, volume, order book and yields |
111
+ | `ifb_history` | get daily prices and YTM |
112
+ | `ifb_ipos` | list finished IPOs or those upcoming and under way |
113
+ | `ifb_underwritings` | list securities offered by underwriting |
114
+ | `ifb_announcements` | search notices, news and market messages |
115
+ | `ifb_article` | read one news article |
116
+ | `ifb_market_summary` | index, turnover, bond indices, boards and top movers |
117
+ | `ifb_yield_curve` | fitted yield curves for government, non-government and treasury bills |
118
+ | `ifb_sukuk_flows` | daily value of Islamic securities bought and sold by investor type |
119
+ | `ifb_market_watch` | live market watch: top gainers, losers and most traded per board, halted symbols |
120
+ | `ifb_index_history` | daily history of the total, equal-weight, price and sukuk indices with 1d/5d/21d/250d changes |
121
+ | `ifb_corporate_actions` | capital increases with rights windows, board transfers, new listings, moves to Tehran Stock Exchange |
122
+ | `ifb_halted` | symbols halted now with date and reason, and issuers flagged for weak disclosure |
123
+
124
+ ## FAQ
125
+
126
+ **Is it official?** No. It reads the public pages and web service of ifb.ir and is not affiliated with Iran Fara Bourse.
127
+
128
+ **Why do symbols end in a digit?** The exchange lists each paper on several boards (`کارون0731`, `0732`). Take the `id` from `ifb_yields` or `ifb_search`.
129
+
130
+ **Is the market watch included?** Not the live ranking grid, which the site fills through a push connection. Quotes come from the site's JSON service.
131
+
132
+ ## فارسی
133
+
134
+ <div dir="rtl">
135
+
136
+ **ifb-mcp** یک سرور MCP غیررسمی و فقط‌خواندنی برای فرابورس ایران (ifb.ir) است. با آن می‌توان اوراق بدهی، صکوک و اسناد خزانه (اخزا) را بر پایهٔ بازده تا سررسید (YTM) مرتب کرد، جزئیات و برنامهٔ پرداخت سود هر ورقه را دید، قیمت و صف خرید و فروش را گرفت، عرضه‌های اولیه و پذیره‌نویسی‌ها را دید، اطلاعیه‌ها و اخبار را جستجو کرد و خلاصهٔ بازار را خواند. کلید API یا حساب لازم نیست. سایت فقط از شبکه‌های ایران پاسخ می‌دهد.
137
+
138
+ نصب: `claude mcp add ifb -- uvx ifb-mcp`
139
+
140
+ </div>
141
+
142
+ ## License
143
+
144
+ MIT
@@ -0,0 +1,116 @@
1
+ <!-- mcp-name: io.github.sepehr071/ifb-mcp -->
2
+
3
+ <div align="center">
4
+
5
+ # ifb-mcp: Iran Fara Bourse (فرابورس ایران) bonds, sukuk and IPO MCP server for AI agents
6
+
7
+ **ifb-mcp is an unofficial, read-only MCP server that gives AI agents the public data of Iran Fara Bourse (ifb.ir), Iran's over-the-counter exchange.**<br>
8
+ Its 16 tools rank bonds, sukuk and treasury bills (اخزا) by yield to maturity, explain a paper's coupons and issuer,<br>
9
+ quote prices and order books, list IPOs and new offerings, search exchange notices and news, and summarise the market.<br>
10
+ No API key or account is needed. It works in Claude, Cursor, Copilot and any other MCP client.
11
+
12
+ [![PyPI](https://img.shields.io/pypi/v/ifb-mcp?color=2563eb)](https://pypi.org/project/ifb-mcp/)
13
+ [![Python](https://img.shields.io/pypi/pyversions/ifb-mcp)](https://pypi.org/project/ifb-mcp/)
14
+ [![License: MIT](https://img.shields.io/badge/license-MIT-16a34a)](https://github.com/sepehr071/ifb-mcp/blob/main/LICENSE)
15
+
16
+ [For AI agents](#for-ai-agents) · [Quick start](#quick-start) · [What it can do](#what-it-can-do) · [Tools](#tools) · [FAQ](#faq) · [فارسی](#فارسی)
17
+
18
+ </div>
19
+
20
+ ---
21
+
22
+ ## For AI agents
23
+
24
+ - **What:** unofficial MCP server (model context protocol) for ifb.ir: yield to maturity of every treasury bill, sukuk and bond, instrument details with coupon schedules, live quotes and order books, price and yield history, IPOs (finished and upcoming), underwritings, exchange announcements, news and market messages, market summary, the live market watch with top movers and most traded, index history, halted symbols, capital increases and board transfers, yield curves and who trades Islamic securities.
25
+ - **Install:** `uvx ifb-mcp` or `claude mcp add ifb -- uvx ifb-mcp`. Transport: stdio. Needs [uv](https://docs.astral.sh/uv/getting-started/installation/) (Python 3.10+).
26
+ - **Auth:** none. Optional `IFB_MCP_PROXY` (HTTP proxy URL, or a comma-separated list); system proxy settings are ignored.
27
+ - **Access:** read-only. It cannot trade, log in or change anything, so no confirmation is needed before calling any tool.
28
+ - **Tools:** 16, all prefixed `ifb_`. Typical sequences: `ifb_yields` -> `ifb_instrument` / `ifb_quote`; `ifb_search` -> `ifb_history`; `ifb_ipos(stage="pipeline")`; `ifb_announcements` -> `ifb_article`.
29
+ - **Use when:** the user asks which bond, sukuk or treasury bill yields most or matures when, what a paper pays and when, a Fara Bourse price or order book, what IPOs are coming or happened, why a symbol is halted, or how the market did.
30
+ - **Do not use when:** the question is about the Tehran Stock Exchange (tse.ir), cryptocurrencies or gold; or the user wants to place an order.
31
+ - **Output:** compact JSON. Prices in rial, yields in percent a year, dates Jalali (`1405/08/09`). Texts are the exchange's own (Persian). The site is reachable from Iranian networks only.
32
+ - **Machine-readable:** [`server.json`](https://github.com/sepehr071/ifb-mcp/blob/main/server.json).
33
+
34
+ ## Why
35
+
36
+ On ifb.ir, "which treasury bill maturing before next spring pays the best yield?" means opening the yield page,
37
+ scrolling 1,000 rows and sorting by hand. With `ifb-mcp` an agent answers it in one call:
38
+
39
+ > **You:** Best yield among treasury bills maturing before 1405/12/01?
40
+ >
41
+ > **Agent:** *calls* `ifb_yields(kind="treasury", matures_before="1405/12/01", limit=3)`
42
+
43
+ <sub>Data is the exchange's, as shown on its website; yields are computed by Fara Bourse from the last trade. This is information, not investment advice.</sub>
44
+
45
+ ## What it can do
46
+
47
+ - **Rank fixed income by yield**: treasury bills, sukuk, participation papers and other bonds with last price, maturity and YTM, filtered by yield, maturity and symbol
48
+ - **Explain a paper**: coupon rate, maturity, issuer, guarantor, underwriter, market maker, full coupon schedule and the next payment
49
+ - **Quote and chart**: live price, change, volume, order book with the yield of each bid and offer, and daily history with YTM
50
+ - **Follow offerings**: finished IPOs with price and volume (filter by market and pricing method), companies being introduced, underwritings
51
+ - **Read the exchange**: search official notices, news and market messages (halts, openings) by keyword and date, and read an article
52
+ - **Watch the live market**: every symbol's price, change and best bid/ask ranked by change or value, per board; halted symbols with reasons; index history since 2009; capital increases and board transfers
53
+ - **See the market**: index, turnover, bond indices, each board's value and top movers, the fitted yield curves and who buys and sells Islamic securities
54
+ - **Read-only by design**: no login, no orders
55
+
56
+ ## Quick start
57
+
58
+ You need [uv](https://docs.astral.sh/uv/getting-started/installation/). No API key or account. ifb.ir answers Iranian
59
+ networks; from abroad set `IFB_MCP_PROXY` to an Iranian proxy.
60
+
61
+ ```bash
62
+ claude mcp add ifb -- uvx ifb-mcp
63
+ ```
64
+
65
+ Claude Desktop, Cursor and others:
66
+
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "ifb": { "command": "uvx", "args": ["ifb-mcp"] }
71
+ }
72
+ }
73
+ ```
74
+
75
+ ## Tools
76
+
77
+ | Tool | Use it to |
78
+ |---|---|
79
+ | `ifb_search` | turn a symbol or name into an instrument id |
80
+ | `ifb_yields` | rank bonds, sukuk and treasury bills by YTM with maturity and price filters |
81
+ | `ifb_instrument` | read coupon rate, maturity, issuer, guarantor and the coupon schedule |
82
+ | `ifb_quote` | get the live price, volume, order book and yields |
83
+ | `ifb_history` | get daily prices and YTM |
84
+ | `ifb_ipos` | list finished IPOs or those upcoming and under way |
85
+ | `ifb_underwritings` | list securities offered by underwriting |
86
+ | `ifb_announcements` | search notices, news and market messages |
87
+ | `ifb_article` | read one news article |
88
+ | `ifb_market_summary` | index, turnover, bond indices, boards and top movers |
89
+ | `ifb_yield_curve` | fitted yield curves for government, non-government and treasury bills |
90
+ | `ifb_sukuk_flows` | daily value of Islamic securities bought and sold by investor type |
91
+ | `ifb_market_watch` | live market watch: top gainers, losers and most traded per board, halted symbols |
92
+ | `ifb_index_history` | daily history of the total, equal-weight, price and sukuk indices with 1d/5d/21d/250d changes |
93
+ | `ifb_corporate_actions` | capital increases with rights windows, board transfers, new listings, moves to Tehran Stock Exchange |
94
+ | `ifb_halted` | symbols halted now with date and reason, and issuers flagged for weak disclosure |
95
+
96
+ ## FAQ
97
+
98
+ **Is it official?** No. It reads the public pages and web service of ifb.ir and is not affiliated with Iran Fara Bourse.
99
+
100
+ **Why do symbols end in a digit?** The exchange lists each paper on several boards (`کارون0731`, `0732`). Take the `id` from `ifb_yields` or `ifb_search`.
101
+
102
+ **Is the market watch included?** Not the live ranking grid, which the site fills through a push connection. Quotes come from the site's JSON service.
103
+
104
+ ## فارسی
105
+
106
+ <div dir="rtl">
107
+
108
+ **ifb-mcp** یک سرور MCP غیررسمی و فقط‌خواندنی برای فرابورس ایران (ifb.ir) است. با آن می‌توان اوراق بدهی، صکوک و اسناد خزانه (اخزا) را بر پایهٔ بازده تا سررسید (YTM) مرتب کرد، جزئیات و برنامهٔ پرداخت سود هر ورقه را دید، قیمت و صف خرید و فروش را گرفت، عرضه‌های اولیه و پذیره‌نویسی‌ها را دید، اطلاعیه‌ها و اخبار را جستجو کرد و خلاصهٔ بازار را خواند. کلید API یا حساب لازم نیست. سایت فقط از شبکه‌های ایران پاسخ می‌دهد.
109
+
110
+ نصب: `claude mcp add ifb -- uvx ifb-mcp`
111
+
112
+ </div>
113
+
114
+ ## License
115
+
116
+ MIT
@@ -0,0 +1,56 @@
1
+ [project]
2
+ name = "ifb-mcp"
3
+ version = "0.1.0"
4
+ description = "Unofficial read-only MCP server for Iran Fara Bourse (ifb.ir): bond and sukuk yields, treasury bills, IPOs, announcements, market stats."
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", "ifb", "fara-bourse", "farabourse", "iran-fara-bourse", "otc", "bonds", "sukuk", "treasury-bills", "ytm", "ipo", "iran", "bourse", "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 :: Office/Business :: Financial :: Investment",
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/ifb-mcp"
32
+ Issues = "https://github.com/sepehr071/ifb-mcp/issues"
33
+ Website = "https://mcpfarsi.ir/"
34
+
35
+ [project.scripts]
36
+ ifb-mcp = "ifb_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 ifb.ir site (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/ifb-mcp",
4
+ "title": "Iran Fara Bourse",
5
+ "description": "Read-only Iran Fara Bourse (ifb.ir): bond, sukuk and treasury yields, IPOs, notices, market stats.",
6
+ "repository": {
7
+ "url": "https://github.com/sepehr071/ifb-mcp",
8
+ "source": "github"
9
+ },
10
+ "websiteUrl": "https://github.com/sepehr071/ifb-mcp",
11
+ "version": "0.1.0",
12
+ "packages": [
13
+ {
14
+ "registryType": "pypi",
15
+ "registryBaseUrl": "https://pypi.org",
16
+ "identifier": "ifb-mcp",
17
+ "version": "0.1.0",
18
+ "runtimeHint": "uvx",
19
+ "transport": {
20
+ "type": "stdio"
21
+ },
22
+ "environmentVariables": [
23
+ {
24
+ "name": "IFB_MCP_PROXY",
25
+ "description": "Optional HTTP proxy URL for the ifb.ir requests; system proxy settings are ignored. Also accepts a comma-separated list: one is used until it gets blocked or fails to connect, then the next.",
26
+ "isRequired": false,
27
+ "isSecret": false
28
+ }
29
+ ]
30
+ }
31
+ ]
32
+ }
@@ -0,0 +1,5 @@
1
+ """Unofficial MCP server for Iran Fara Bourse (ifb.ir): bond and sukuk yields, IPOs, announcements, market stats."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("ifb-mcp")
@@ -0,0 +1,113 @@
1
+ """Reads ifb.ir's server-rendered tables and pages through its WebForms grids."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from html.parser import HTMLParser
7
+
8
+ from . import http
9
+
10
+ CP = "ctl00$ContentPlaceHolder1$" # prefix of every control name on the pages
11
+
12
+
13
+ class Row(list):
14
+ """The text of each cell; `hrefs` holds the first link of each cell (or None)."""
15
+
16
+ hrefs: list[str | None]
17
+
18
+
19
+ class Table:
20
+ def __init__(self, tid: str | None, cls: str | None = None):
21
+ self.id = tid
22
+ self.cls = cls or ""
23
+ self.rows: list[Row] = []
24
+
25
+
26
+ class _Parser(HTMLParser):
27
+ def __init__(self):
28
+ super().__init__(convert_charrefs=True)
29
+ self.tables: list[Table] = []
30
+ self._stack: list[dict] = []
31
+
32
+ def handle_starttag(self, tag, attrs):
33
+ a = dict(attrs)
34
+ if tag == "table":
35
+ t = Table(a.get("id"), a.get("class"))
36
+ self.tables.append(t)
37
+ self._stack.append({"t": t, "row": None, "cell": None})
38
+ elif not self._stack:
39
+ return
40
+ elif tag == "tr":
41
+ row = Row()
42
+ row.hrefs = []
43
+ self._stack[-1]["row"] = row
44
+ elif tag in ("td", "th") and self._stack[-1]["row"] is not None:
45
+ self._stack[-1]["cell"] = {"text": "", "href": None}
46
+ elif tag == "a" and (c := self._stack[-1]["cell"]) is not None and c["href"] is None and a.get("href"):
47
+ c["href"] = a["href"]
48
+ elif tag == "br" and (c := self._stack[-1]["cell"]) is not None:
49
+ c["text"] += " "
50
+
51
+ def handle_endtag(self, tag):
52
+ if not self._stack:
53
+ return
54
+ top = self._stack[-1]
55
+ if tag in ("td", "th") and top["cell"] is not None and top["row"] is not None:
56
+ top["row"].append(re.sub(r"\s+", " ", top["cell"]["text"]).strip())
57
+ top["row"].hrefs.append(top["cell"]["href"])
58
+ top["cell"] = None
59
+ elif tag == "tr" and top["row"] is not None:
60
+ if top["row"]:
61
+ top["t"].rows.append(top["row"])
62
+ top["row"] = None
63
+ elif tag == "table":
64
+ self._stack.pop()
65
+
66
+ def handle_data(self, data):
67
+ if self._stack and (c := self._stack[-1]["cell"]) is not None:
68
+ c["text"] += data
69
+
70
+
71
+ def tables(html: str) -> list[Table]:
72
+ p = _Parser()
73
+ p.feed(html)
74
+ return p.tables
75
+
76
+
77
+ def table(html: str, suffix: str) -> Table | None:
78
+ """The table whose id ends with `suffix` (e.g. "grdIPOs")."""
79
+ return next((t for t in tables(html) if t.id and t.id.endswith(suffix)), None)
80
+
81
+
82
+ def pairs(html: str) -> dict[str, str]:
83
+ """Label/value rows of the `insTable` blocks on an instrument page (first value wins)."""
84
+ out: dict[str, str] = {}
85
+ for t in tables(html):
86
+ if t.cls.startswith("insTable"):
87
+ for r in t.rows:
88
+ if len(r) == 2 and r[0] and r[0] not in out:
89
+ out[r[0]] = r[1]
90
+ return out
91
+
92
+
93
+ def hrefid(href: str | None) -> int | None:
94
+ """The instrument id in a link like Instruments.aspx?id=123."""
95
+ m = re.search(r"id=(\d+)", href or "")
96
+ return int(m.group(1)) if m else None
97
+
98
+
99
+ async def grid(path: str, name: str, page: int = 1, form: dict[str, str] | None = None) -> tuple[Table | None, str]:
100
+ """One page of the WebForms grid control `name` on `path`, after an optional search `form` (control -> value).
101
+
102
+ Searches and pager clicks are postbacks that need the page's hidden state, so this is 1 to 3 requests.
103
+ Returns the table and the final HTML.
104
+ """
105
+ html = await http.get_page(path)
106
+ fields = {k: v for k, v in (form or {}).items()}
107
+ if fields:
108
+ html = await http.post_page(path, {**http.hidden_fields(html), **fields})
109
+ if page > 1:
110
+ target = {"__EVENTTARGET": f"{CP}{name}", "__EVENTARGUMENT": f"Page${page}"}
111
+ keep = {k: v for k, v in fields.items() if not k.endswith("BtnSearch")} # the button would re-run the search
112
+ html = await http.post_page(path, {**http.hidden_fields(html), **keep, **target})
113
+ return table(html, name), html