Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions chatgpt/build_instructions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
])


Expand Down
1 change: 1 addition & 0 deletions docs/mcp-original-readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 對應段落讀。

Expand Down
2 changes: 1 addition & 1 deletion knowledge/datasets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 以上
Expand Down
10 changes: 5 additions & 5 deletions knowledge/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: []`)

Expand Down Expand Up @@ -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 不需更換,立即生效。

Expand Down
38 changes: 38 additions & 0 deletions knowledge/pricing.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions skills/finmind/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`(會員中心 <https://finmindtrade.com/analysis/#/account/user> 取得)。
未設定或 401 時,引導使用者去該頁取得 Token 並設成環境變數。
Expand Down
4 changes: 2 additions & 2 deletions smoke.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
Expand Down
6 changes: 4 additions & 2 deletions src/finmind_mcp/knowledge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -41,6 +42,7 @@ def _find_knowledge_dir() -> Path:
"instructions": "instructions.md",
"token-guide": "token-guide.md",
"regression": "regression.md",
"pricing": "pricing.md",
}


Expand Down Expand Up @@ -172,14 +174,14 @@ 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": (
"FinMind 服務目前回應較慢或暫時無法連線,請稍候再試。"
),
"rate_limit": (
"您目前的請求數已達 FinMind 方案上限。"
"升級方案:https://finmindtrade.com/analysis/#/account/pricing"
"升級方案:https://finmindtrade.com/analysis/#/Sponsor/sponsor"
),
}
2 changes: 1 addition & 1 deletion src/finmind_mcp/server.py
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
32 changes: 28 additions & 4 deletions src/finmind_mcp/tools.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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": {}},
),
]


Expand All @@ -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,
}
Expand Down Expand Up @@ -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()
Expand All @@ -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(
Expand Down
14 changes: 14 additions & 0 deletions tests/test_knowledge.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
28 changes: 26 additions & 2 deletions tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,15 @@ 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 == [
"query_dataset",
"list_datasets",
"get_stock_info",
"query_trading_daily_report",
"get_pricing",
]
for d in defs:
assert d.description
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down
Loading