diff --git a/chatgpt/build_instructions.py b/chatgpt/build_instructions.py index c09fa37..99a2dba 100644 --- a/chatgpt/build_instructions.py +++ b/chatgpt/build_instructions.py @@ -116,10 +116,12 @@ def build_bundle() -> str: datasets = (KNOWLEDGE / "datasets.md").read_text(encoding="utf-8").strip() examples = (KNOWLEDGE / "examples.md").read_text(encoding="utf-8").strip() token_guide = (KNOWLEDGE / "token-guide.md").read_text(encoding="utf-8").strip() + pricing = (KNOWLEDGE / "pricing.md").read_text(encoding="utf-8").strip() return "\n\n---\n\n".join([ "# FinMind Custom GPT — 完整 Dataset 參考\n\n" + datasets, "# 範例問答\n\n" + examples, "# Token 取得與設定\n\n" + token_guide, + pricing, ]) diff --git a/docs/mcp-original-readme.md b/docs/mcp-original-readme.md index e645670..eb88b30 100644 --- a/docs/mcp-original-readme.md +++ b/docs/mcp-original-readme.md @@ -11,6 +11,7 @@ AI tools (Claude Desktop / Code, Gemini CLI, Cursor, Windsurf, Codex). | `list_datasets` | bundled `knowledge/datasets.md`(無 API) | — | | `get_stock_info` | `/api/v4/data?dataset=TaiwanStockInfo` | — | | `query_trading_daily_report` | `/api/v4/taiwan_stock_trading_daily_report` | `data_id`, `date` | +| `get_pricing` | bundled `knowledge/pricing.md`(無 API、無需 token) | — | Each tool returns a markdown table; results over 500 rows are truncated with a note pointing to the host's code interpreter. diff --git a/docs/spec.md b/docs/spec.md index 36d0fa1..fd390ac 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -212,6 +212,7 @@ Claude.ai / Code / Desktop Gemini CLI / Cursor / Windsurf | `list_datasets` | 列可用 dataset 與層級 | 內建 `knowledge/datasets.md`(無 API) | | `query_trading_daily_report` | dedicated dataset,必填 `data_id` + 單日 `date` | `/api/v4/taiwan_stock_trading_daily_report` | | `get_stock_info` | 股票代號 ↔ 中文名查詢 | `/api/v4/data?dataset=TaiwanStockInfo` | +| `get_pricing` | 方案價格、API 上限、授權規則 | 內建 `knowledge/pricing.md`(無 API、無需 token) | 每個 tool 的 description 從 `knowledge/datasets.md` 對應段落讀。 diff --git a/knowledge/datasets.md b/knowledge/datasets.md index 0e75bbe..5c30826 100644 --- a/knowledge/datasets.md +++ b/knowledge/datasets.md @@ -4,7 +4,7 @@ ChatGPT Custom GPT 與 MCP server 共用此文件作為 single source of truth ## Tier 說明 -FinMind 會員方案由低到高為 **Free → Backer → Sponsor → Sponsor Pro**;高階方案涵蓋所有低階方案可存取的資料。下方每個 dataset 標示的是「可存取所需的最低方案」。 +FinMind 會員方案由低到高為 **Free → Backer → Sponsor → Sponsor Pro**;高階方案涵蓋所有低階方案可存取的資料。下方每個 dataset 標示的是「可存取所需的最低方案」。各方案價格、API 上限與授權規則見 `pricing.md`(MCP tool `get_pricing`)。 - **Free:** 不需 `data_id` 也能查(多為總覽 / 整體市場 / 列表型 dataset) - **Free(w/ data_id):** 帶 `data_id`(單一標的)為 Free;省略 `data_id` 想查全市場需 Backer 以上 diff --git a/knowledge/errors.md b/knowledge/errors.md index 4832714..a0b3583 100644 --- a/knowledge/errors.md +++ b/knowledge/errors.md @@ -25,13 +25,13 @@ > > 此資料集屬於進階資料(例如分點進出、即時報價、鉅額交易、借貸款項擔保品餘額等),免費與 Backer 方案無法存取。 > -> 升級 Sponsor 方案:https://finmindtrade.com/analysis/#/account/pricing +> 升級 Sponsor 方案:https://finmindtrade.com/analysis/#/Sponsor/sponsor > > 升級後 Token 不需更換,立即生效。 **下一步建議:** - 若使用者只是想試水溫,可建議改查同主題的免費 dataset(例如 sponsor 的 `TaiwanStockTradingDailyReport` 改用免費的 `TaiwanStockInstitutionalInvestorsBuySell`)。 -- 列出 FinMind 各方案差異,協助使用者判斷。 +- 列出 FinMind 各方案價格與差異(見 pricing),協助使用者判斷。 ## 空資料(`data: []`) @@ -68,20 +68,20 @@ ## Rate Limit(429 或請求數達上限) -**觸發條件:** FinMind API 回應 HTTP 429,或回傳訊息提示請求數達上限。各方案上限:Free 600 req/hr、Backer 1,600 req/hr、Sponsor 6,000 req/hr、SponsorPro 20,000 req/hr。 +**觸發條件:** FinMind API 回應 HTTP 429,或回傳訊息提示請求數達上限(各方案上限見下方模板)。 **回應模板:** > 您目前的請求數已達 FinMind 方案上限。 > > 各方案每小時請求數: -> - Free:600 次 +> - Free:300 次(註冊帶 token 600 次) > - Backer:1,600 次 > - Sponsor:6,000 次 > - SponsorPro:20,000 次 > > 解法: > 1. 等待 1 小時後配額會重置 -> 2. 升級方案以提高上限:https://finmindtrade.com/analysis/#/account/pricing +> 2. 升級方案以提高上限:https://finmindtrade.com/analysis/#/Sponsor/sponsor > > 升級後 Token 不需更換,立即生效。 diff --git a/knowledge/pricing.md b/knowledge/pricing.md new file mode 100644 index 0000000..7ecccfb --- /dev/null +++ b/knowledge/pricing.md @@ -0,0 +1,38 @@ +# FinMind 方案與價格 + +所有價格皆為**新台幣(TWD)**。購買/升級頁面:https://finmindtrade.com/analysis/#/Sponsor/sponsor +完整說明(中文):https://finmind.github.io/Pricing/ ;English:https://finmind.github.io/en/Pricing/ + +## 方案總覽 + +| 方案 | 月繳 | 年繳 | API/下載上限 | 資料集數 | 使用授權 | +|---|---|---|---|---|---| +| Free | 免費 | 免費 | 300 次/小時(註冊會員帶 token 600 次/小時) | 45 種 | 非商業用途 | +| Backer | NT$699 | NT$5,499 | 1,600 次/小時 | 81 種 | 非商業用途 | +| Sponsor(建議方案) | NT$999 | NT$8,888 | 6,000 次/小時 | 97 種 | 非商業用途 | +| Sponsor Pro | NT$3,330 | NT$29,620 | 20,000 次/小時 | 97 種+整日全商品下載 | 商業用途 | + +- 高階方案包含低階方案所有權限(Backer ⊃ Free、Sponsor ⊃ Backer、Sponsor Pro ⊃ Sponsor)。 +- 每個 dataset 所需的最低方案,標示在 `list_datasets` 結果與 datasets.md 的 Tier 欄。 + +## 付款與升級 + +- **單次付款,不會自動扣款**:到期後自動回到 Free,無需取消;要續用再次手動付款。 +- **可隨時升級**:已購買 Backer 或 Sponsor,可依剩餘天數補差額升級(最低 NT$100)。 +- 升級後 Token 不需更換,立即生效。 +- 如需統編發票,於訂單確認頁輸入統編。學校單位可於購買頁下載校園推廣方案說明。 + +## 各方案新增重點 + +- **Backer**:不帶 `data_id` 一次查特定日期全部股票(股價、三大法人、融資融券等);還原股價、台股歷史逐筆、週 K/月 K、漲跌停價、期貨/選擇權交易明細、大額交易人未沖銷部位、夜盤三大法人、可轉債全系列、景氣對策信號、產業鏈、恐懼與貪婪指數、美股分 K 等。 +- **Sponsor**:台股分 K、分點進出、權證標的對照、八大行庫、鉅額交易、借貸款項擔保品餘額、個股融資維持率、主動式 ETF 持股明細/異動、產業鏈資金流向、台股/期貨/選擇權即時資訊、期貨價差逐筆、期貨分 K。 +- **Sponsor Pro**:商業授權,以及特定日期全部商品整日下載(台股逐筆、分點、權證分點、分 K、期貨/選擇權交易明細)。 + +## 使用授權 + +- Free/Backer/Sponsor:限個人、學術、web/app 開發等**非商業用途**。 +- Sponsor Pro:可用於 SaaS、AI、web/app、公司內部系統等**商業用途**,可整合至商業產品對外販售。 +- 所有方案皆**禁止**再散布原始資料、建立鏡像 API,也不得將即時資料直接呈現於 web、App 等對外介面;皆需標示資料來源「FinMind」。 +- 使用者問「要做商業產品選哪個」→ Sponsor Pro;「不確定選哪個」→ 可先從 Backer 開始,之後補差額升級。 + +其他問題:finmind.tw@gmail.com diff --git a/skills/finmind/SKILL.md b/skills/finmind/SKILL.md index fb31b45..ae6137d 100644 --- a/skills/finmind/SKILL.md +++ b/skills/finmind/SKILL.md @@ -12,6 +12,7 @@ description: Use when the user asks about Taiwan or global financial data — - `get_stock_info` — 台股代號/中文名/產業查詢(TaiwanStockInfo) - `list_datasets` — 列出所有可用 dataset;不確定用哪個時先查 - `query_trading_daily_report` — 券商分點進出(Sponsor 等級,必填 `data_id` + 單日 `date`) +- `get_pricing` — 方案價格、API 上限、授權規則;使用者問價格/方案/升級時用(不需 token) 需要 `FINMIND_TOKEN`(會員中心 取得)。 未設定或 401 時,引導使用者去該頁取得 Token 並設成環境變數。 diff --git a/smoke.py b/smoke.py index 3542065..8ed8d80 100644 --- a/smoke.py +++ b/smoke.py @@ -1,5 +1,5 @@ """Launches the FinMind MCP server as a subprocess, sends initialize + -tools/list over stdio, and asserts exactly 4 tools are registered. +tools/list over stdio, and asserts exactly 5 tools are registered. Usage: uv run python smoke.py @@ -57,7 +57,7 @@ async def recv() -> dict: tools_list = tools_resp["result"]["tools"] names = [t["name"] for t in tools_list] print("tools:", names) - assert len(tools_list) == 4, f"expected 4, got {len(tools_list)}: {names}" + assert len(tools_list) == 5, f"expected 5, got {len(tools_list)}: {names}" proc.terminate() await proc.wait() diff --git a/src/finmind_mcp/knowledge.py b/src/finmind_mcp/knowledge.py index 30d7d3d..5bd03f2 100644 --- a/src/finmind_mcp/knowledge.py +++ b/src/finmind_mcp/knowledge.py @@ -7,6 +7,7 @@ finmind://instructions → knowledge/instructions.md finmind://token-guide → knowledge/token-guide.md finmind://regression → knowledge/regression.md + finmind://pricing → knowledge/pricing.md """ from __future__ import annotations @@ -41,6 +42,7 @@ def _find_knowledge_dir() -> Path: "instructions": "instructions.md", "token-guide": "token-guide.md", "regression": "regression.md", + "pricing": "pricing.md", } @@ -172,7 +174,7 @@ def get_error_template(kind: str) -> Optional[str]: ), "payment": ( "您查詢的資料集需要 Sponsor 會員權限才能存取。" - "升級 Sponsor 方案:https://finmindtrade.com/analysis/#/account/pricing" + "升級 Sponsor 方案:https://finmindtrade.com/analysis/#/Sponsor/sponsor" ), "empty": "在指定條件下查無資料,請確認股票代號與日期區間。", "upstream": ( @@ -180,6 +182,6 @@ def get_error_template(kind: str) -> Optional[str]: ), "rate_limit": ( "您目前的請求數已達 FinMind 方案上限。" - "升級方案:https://finmindtrade.com/analysis/#/account/pricing" + "升級方案:https://finmindtrade.com/analysis/#/Sponsor/sponsor" ), } diff --git a/src/finmind_mcp/server.py b/src/finmind_mcp/server.py index 6d1a086..823238e 100644 --- a/src/finmind_mcp/server.py +++ b/src/finmind_mcp/server.py @@ -1,6 +1,6 @@ """FinMind MCP server entry point (stdio transport). -Registers four tools and a small set of markdown resources from the +Registers five tools and a small set of markdown resources from the shared knowledge pack. Designed to be launched by an MCP host such as Claude Desktop, Claude Code, Cursor, Windsurf, or Gemini CLI. diff --git a/src/finmind_mcp/tools.py b/src/finmind_mcp/tools.py index 83b2785..1109fd3 100644 --- a/src/finmind_mcp/tools.py +++ b/src/finmind_mcp/tools.py @@ -1,11 +1,12 @@ """MCP tool implementations and markdown formatting. -Four tools are exposed: +Five tools are exposed: - `query_dataset` — generic /api/v4/data query - `list_datasets` — bundled dataset catalog (knowledge/datasets.md) - `get_stock_info` — shorthand for TaiwanStockInfo - `query_trading_daily_report` — dedicated /api/v4/taiwan_stock_trading_daily_report +- `get_pricing` — bundled plan pricing (knowledge/pricing.md) Each tool returns a markdown string. `FinMindError` subclasses raised by `FinMindClient` are caught and converted into the 繁中 templates from @@ -43,7 +44,7 @@ def _make_client() -> FinMindClient: def tool_definitions() -> list[Tool]: - """Return MCP Tool definitions for all four tools in stable order.""" + """Return MCP Tool definitions for all five tools in stable order.""" return [ Tool( name="query_dataset", @@ -122,6 +123,16 @@ def tool_definitions() -> list[Tool]: "required": ["data_id", "date"], }, ), + Tool( + name="get_pricing", + description=( + "查詢 FinMind 會員方案價格(讀取內建知識庫,不需連線、不需 token)。" + "回傳 Free / Backer / Sponsor / Sponsor Pro 的月繳/年繳價格(新台幣)、" + "API 每小時上限、各方案新增資料集、商業授權規則與購買連結。" + "使用者問價格、費用、方案差異、升級或商業用途時使用。" + ), + inputSchema={"type": "object", "properties": {}}, + ), ] @@ -134,9 +145,15 @@ async def dispatch(name: str, arguments: dict[str, Any]) -> str: Returns a markdown string. Errors from the upstream client are caught and rendered as 繁中 user-facing messages. """ + # Offline tools read the bundled knowledge pack and need no token. + offline = { + "list_datasets": _list_datasets, + "get_pricing": _get_pricing, + } + if name in offline: + return offline[name]() handlers = { "query_dataset": _query_dataset, - "list_datasets": _list_datasets, "get_stock_info": _get_stock_info, "query_trading_daily_report": _query_trading_daily_report, } @@ -169,7 +186,7 @@ async def _query_dataset(client: FinMindClient, args: dict[str, Any]) -> str: return _format_markdown_table(rows, title=dataset) -async def _list_datasets(_client: FinMindClient, _args: dict[str, Any]) -> str: +def _list_datasets() -> str: # FinMind has no "list all datasets" endpoint; read the bundled catalog # (datasets.md) — the same SSOT as the Custom GPT knowledge bundle. catalog = knowledge.dataset_catalog() @@ -187,6 +204,13 @@ async def _list_datasets(_client: FinMindClient, _args: dict[str, Any]) -> str: return "\n".join(lines) +def _get_pricing() -> str: + try: + return knowledge.read("finmind://pricing") + except (KeyError, ValueError, OSError): + return "方案價格請見:https://finmindtrade.com/analysis/#/Sponsor/sponsor" + + async def _get_stock_info(client: FinMindClient, args: dict[str, Any]) -> str: stock_id = args.get("stock_id") rows = await client.query_dataset( diff --git a/tests/test_knowledge.py b/tests/test_knowledge.py index 843963b..ccbe95a 100644 --- a/tests/test_knowledge.py +++ b/tests/test_knowledge.py @@ -23,3 +23,17 @@ def test_dataset_catalog_captures_tier_and_category(): assert "Free" in price["tier"] # Legend section "Tier 說明" must never produce a dataset record. assert "Tier 說明" not in {rec["category"] for rec in by_name.values()} + + +def test_pricing_resource_is_registered_and_readable(): + uris = [str(r.uri) for r in knowledge.resource_definitions()] + assert "finmind://pricing" in uris + md = knowledge.read("finmind://pricing") + assert "Sponsor Pro" in md + assert "https://finmindtrade.com/analysis/#/Sponsor/sponsor" in md + + +def test_no_dead_upgrade_link_in_knowledge_pack(): + # /account/pricing is not a real route on finmindtrade.com. + for r in knowledge.resource_definitions(): + assert "#/account/pricing" not in knowledge.read(str(r.uri)) diff --git a/tests/test_tools.py b/tests/test_tools.py index 8ecd2df..78f0f5d 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -52,7 +52,7 @@ def _install(**kwargs): return _install -def test_tool_definitions_returns_four_tools(): +def test_tool_definitions_returns_five_tools(): defs = tools.tool_definitions() names = [d.name for d in defs] assert names == [ @@ -60,6 +60,7 @@ def test_tool_definitions_returns_four_tools(): "list_datasets", "get_stock_info", "query_trading_daily_report", + "get_pricing", ] for d in defs: assert d.description @@ -133,6 +134,28 @@ async def test_dispatch_list_datasets_reads_knowledge_pack(install_fake_client): assert client.calls == [] +@pytest.mark.asyncio +async def test_dispatch_get_pricing_reads_knowledge_pack(install_fake_client): + client = install_fake_client() + md = await tools.dispatch("get_pricing", {}) + for plan in ("Free", "Backer", "Sponsor", "Sponsor Pro"): + assert plan in md + assert "NT$699" in md and "NT$999" in md and "NT$3,330" in md + assert "https://finmindtrade.com/analysis/#/Sponsor/sponsor" in md + assert client.calls == [] + + +@pytest.mark.asyncio +async def test_offline_tools_work_without_token(monkeypatch): + # Pricing and the dataset catalog must be answerable before the user + # has a token (e.g. someone deciding which plan to buy). + monkeypatch.delenv("FINMIND_TOKEN", raising=False) + pricing = await tools.dispatch("get_pricing", {}) + assert "NT$999" in pricing + catalog = await tools.dispatch("list_datasets", {}) + assert "TaiwanStockPrice" in catalog + + @pytest.mark.asyncio async def test_dispatch_get_stock_info_calls_taiwanstockinfo(install_fake_client): client = install_fake_client( @@ -219,7 +242,8 @@ async def test_payment_required_returns_user_facing_message(install_fake_client) {"dataset": "TaiwanStockBlockTrade", "data_id": "2330", "start_date": "2026-05-10"}, ) assert "Sponsor" in md - assert "pricing" in md + assert "#/Sponsor/sponsor" in md + assert "account/pricing" not in md @pytest.mark.asyncio