From 7f835bad39649060667b069f8ace08c0b4176341 Mon Sep 17 00:00:00 2001 From: linsamtw Date: Thu, 1 Oct 2026 18:27:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20Claude=20Desktop=20=E4=B8=80=E9=8D=B5?= =?UTF-8?q?=E5=AE=89=E8=A3=9D=E6=93=B4=E5=85=85=E5=A5=97=E4=BB=B6=EF=BC=88?= =?UTF-8?q?finmind.mcpb=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用戶反映 PowerShell 找得到 uvx 但 Claude Desktop 找不到,手動查路徑、 改 JSON 對一般用戶太複雜。改提供 Claude Desktop 擴充套件:下載 .mcpb 點兩下、貼 token 即可,不需安裝 Python、不需編輯設定檔、不碰 PATH。 新增檔案: - mcpb/manifest.json:manifest v0.4,server.type = "uv"(由 Claude Desktop 自行以 uv 準備 Python 與相依套件);user_config.finmind_token(必填、 sensitive)經 env 傳成 FINMIND_TOKEN;列出 5 個 tool。刻意不宣告 compatibility.runtimes.python,避免 Claude Desktop 檢查系統 Python 後拒裝 - mcpb/pyproject.toml:相依 finmind-mcp==<版本>,打包時改成發版 tag - mcpb/src/server.py:進入點,呼叫 finmind_mcp.server.main - mcpb/icon.png:512x512,由 FinMind logo 裁切 - mcpb/.mcpbignore - scripts/build_mcpb.py:複製 mcpb/ → 寫入版本 → npx @anthropic-ai/mcpb@2 validate + pack → dist/finmind.mcpb;--dev 讓相依指向本機原始碼 - scripts/check_mcpb.py:模擬 host(解壓、代入 ${__dirname}/${user_config.*}、 以 uv 啟動、MCP 握手、tools/list 對照 manifest、呼叫 list_datasets) - tests/test_mcpb.py:manifest tools 與 server 一致、uv 類型與 token 對應、 不宣告 runtimes.python、版本寫入與 tag 解析 修改檔案: - .github/workflows/ci.yml:新增 mcpb job(--dev 打包+模擬安裝) - .github/workflows/publish.yml:PyPI 發佈後新增 mcpb job,釘同版本打包、 從 PyPI 模擬安裝(index 延遲重試 5 次),附加到該 tag 的 GitHub Release - install/claude-desktop.md:擴充套件列為方式一(推薦),手動設定改方式二, 新增「擴充套件無法連線」常見問題 - install/windows.md:開頭導向 Claude Desktop 擴充套件;找不到指令的 FAQ 先建議完全結束重開,仍不行才查完整路徑 - README.md:Claude Desktop 一鍵安裝連結、倉庫結構、發佈流程說明 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 23 ++++++++ .github/workflows/publish.yml | 41 ++++++++++++++ README.md | 12 +++- install/claude-desktop.md | 38 ++++++++++--- install/windows.md | 8 ++- mcpb/.mcpbignore | 4 ++ mcpb/icon.png | Bin 0 -> 56784 bytes mcpb/manifest.json | 53 ++++++++++++++++++ mcpb/pyproject.toml | 10 ++++ mcpb/src/server.py | 6 ++ scripts/build_mcpb.py | 82 ++++++++++++++++++++++++++++ scripts/check_mcpb.py | 100 ++++++++++++++++++++++++++++++++++ tests/test_mcpb.py | 68 +++++++++++++++++++++++ 13 files changed, 434 insertions(+), 11 deletions(-) create mode 100644 mcpb/.mcpbignore create mode 100644 mcpb/icon.png create mode 100644 mcpb/manifest.json create mode 100644 mcpb/pyproject.toml create mode 100644 mcpb/src/server.py create mode 100644 scripts/build_mcpb.py create mode 100644 scripts/check_mcpb.py create mode 100644 tests/test_mcpb.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67c205a..61e3ecd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,3 +60,26 @@ jobs: - name: Catalog vs FinMind-Doc sync check run: python scripts/check_doc_sync.py --doc ./FinMind-Doc + + mcpb: + # Claude Desktop 擴充套件(.mcpb):用官方 CLI 驗證 + 打包,再模擬 host 解壓、 + # 以 uv 啟動並走一次 MCP 握手。--dev 讓相依指向本 PR 的原始碼,而非 PyPI 舊版。 + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # --dev 會從原始碼 build 本套件,hatch-vcs 需要 tags + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Build finmind.mcpb + run: python3 scripts/build_mcpb.py --dev + + - name: Install & launch like Claude Desktop + run: python3 scripts/check_mcpb.py diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 84d6799..3838e42 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,6 +18,9 @@ name: Publish to PyPI # - git tag v0.1.0 # - git push origin v0.1.0 ← 推 tag 就觸發;hatch-vcs 從 tag 算出版本 0.1.0 # 自動 build → 上傳 PyPI(PyPI 版本號不可重複,所以每次用新 tag) +# - 上 PyPI 之後,mcpb job 會打包 Claude Desktop 擴充套件 finmind.mcpb(釘同版本), +# 附加到該 tag 的 GitHub Release。下載連結固定為 +# https://github.com/FinMind/FinMind-MCP/releases/latest/download/finmind.mcpb on: push: @@ -66,3 +69,41 @@ jobs: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@release/v1 + + mcpb: + # 必須在 PyPI 發佈之後:.mcpb 裡的 pyproject 釘 finmind-mcp==, + # 使用者安裝時由 Claude Desktop 用 uv 從 PyPI 下載。 + needs: pypi-publish + if: github.ref_type == 'tag' + runs-on: ubuntu-latest + permissions: + contents: write # 建立 / 更新 GitHub Release + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Build finmind.mcpb (pinned to this tag) + run: python3 scripts/build_mcpb.py "$GITHUB_REF_NAME" + + - name: Install & launch like Claude Desktop (from PyPI) + # PyPI 剛發佈時 index 可能還沒更新,失敗就等一下重試 + run: | + for i in 1 2 3 4 5; do + python3 scripts/check_mcpb.py && exit 0 + echo "retry $i: waiting for PyPI index"; sleep 60 + done + exit 1 + + - name: Attach to GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release view "$GITHUB_REF_NAME" >/dev/null 2>&1 \ + || gh release create "$GITHUB_REF_NAME" --verify-tag --generate-notes --title "$GITHUB_REF_NAME" + gh release upload "$GITHUB_REF_NAME" dist/finmind.mcpb --clobber diff --git a/README.md b/README.md index 75cad3b..acafb01 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ pipx install finmind-mcp | Host | 安裝指引 | |---|---| -| Claude Desktop | [install/claude-desktop.md](install/claude-desktop.md) | +| Claude Desktop | **一鍵安裝**:下載 [finmind.mcpb](https://github.com/FinMind/FinMind-MCP/releases/latest/download/finmind.mcpb) 點兩下 → 詳見 [install/claude-desktop.md](install/claude-desktop.md) | | Claude Code | [install/claude-code.md](install/claude-code.md) | | Cursor | [install/cursor.md](install/cursor.md) | | Windsurf | [install/windsurf.md](install/windsurf.md) | @@ -52,6 +52,7 @@ FinMind-MCP/ ├── knowledge/ # SSOT markdown(dataset、行為規則、錯誤腳本、Q&A) ├── chatgpt/ # Custom GPT artifacts(OpenAPI + build script) ├── install/ # 各 host 安裝指引 +├── mcpb/ # Claude Desktop 擴充套件(.mcpb)的 manifest 與進入點 └── docs/ # 設計 spec 與實作 plan ``` @@ -79,6 +80,15 @@ git tag v0.1.0 git push origin v0.1.0 # → CI 自動 build + 發 PyPI ``` +PyPI 發佈成功後,同一個 workflow 會打包 Claude Desktop 擴充套件 `finmind.mcpb`(`finmind-mcp` 釘在同一版本)並附加到該 tag 的 GitHub Release,下載連結 `releases/latest/download/finmind.mcpb` 會自動指向最新版。本機打包測試: + +```bash +python3 scripts/build_mcpb.py --dev # 需要 Node.js;--dev 用本機原始碼 +python3 scripts/check_mcpb.py # 模擬 Claude Desktop 解壓、啟動、MCP 握手 +``` + +新增 tool 時記得同步 `mcpb/manifest.json` 的 `tools`(`tests/test_mcpb.py` 會擋)。 + `knowledge/` 由 hatch `force-include` 打包進 wheel(`finmind_mcp/_knowledge/`),server runtime 直接讀、不用 compile;發版後用戶 `uvx finmind-mcp` / `pipx install finmind-mcp` 即抓到新版。 > 首次需在 PyPI 設好 trusted publisher(project `finmind-mcp` / repo `FinMind/FinMind-MCP` / workflow `publish.yml` / env `pypi`)。 diff --git a/install/claude-desktop.md b/install/claude-desktop.md index c2641e7..6ed76c3 100644 --- a/install/claude-desktop.md +++ b/install/claude-desktop.md @@ -1,10 +1,26 @@ # Claude Desktop -在 Claude Desktop 桌面應用程式啟用 FinMind MCP server(本機 stdio 模式),讓 Claude 直接呼叫 FinMind 金融資料。 +在 Claude Desktop 桌面應用程式啟用 FinMind MCP server,讓 Claude 直接呼叫 FinMind 金融資料。 -## 安裝套件 +先依 [token 取得指引](../knowledge/token-guide.md) 取得 FinMind Token,再選擇以下任一方式安裝。 -請先安裝 `finmind-mcp`(擇一即可): +## 方式一:一鍵安裝擴充套件(推薦) + +不需要安裝 Python、不需要編輯設定檔,Windows / macOS 步驟相同: + +1. 下載 **[finmind.mcpb](https://github.com/FinMind/FinMind-MCP/releases/latest/download/finmind.mcpb)** +2. 在下載的檔案上**點兩下**,Claude Desktop 會跳出安裝視窗(若沒有反應:開啟 Claude Desktop →「設定」→「擴充功能(Extensions)」,把檔案拖進視窗) +3. 按「安裝」,在「FinMind Token」欄位貼上 token,確認啟用 + +第一次使用時 Claude Desktop 會自動下載執行環境,約需數十秒。 + +> 更新版本:重新下載上面的連結再安裝一次即可。 + +## 方式二:手動編輯設定檔 + +適合想自行管理套件版本的使用者。 + +### 安裝套件 ```bash pipx install finmind-mcp # 推薦:長駐安裝 @@ -14,7 +30,7 @@ uvx finmind-mcp --help # 用 uv 即時跑,不安裝 > **Windows 使用者**:`pipx`/`uvx` 的安裝方式、token 設定與常見 PATH 問題,請先看 [Windows 安裝指引](windows.md)。 -## 設定 +### 設定 依您的作業系統,編輯對應的設定檔: @@ -39,12 +55,16 @@ uvx finmind-mcp --help # 用 uv 即時跑,不安裝 } ``` -## 取得 Token - -請先依 [token 取得指引](../knowledge/token-guide.md) 取得 FinMind Token,並設定環境變數 `FINMIND_TOKEN`(或填入上方設定)。 - ## 驗證 -重啟 Claude Desktop,在對話框輸入「列出 FinMind 可用的 dataset」,應該看到工具圖示出現並回傳 dataset 清單。 +(方式二需先完全結束並重開 Claude Desktop)在對話框輸入「列出 FinMind 可用的 dataset」,應該看到工具圖示出現並回傳 dataset 清單。 ![](../../docs/images/install/claude-desktop.png) + +## 常見問題 + +### 擴充套件顯示無法連線 + +1. 確認 token 有貼對:「設定」→「擴充功能」→ FinMind →「設定」重新貼上。 +2. 第一次啟動需要下載執行環境,網路較慢時可能逾時;等一分鐘後關閉再開啟擴充套件。 +3. 仍失敗時,安裝 [uv](https://docs.astral.sh/uv/getting-started/installation/)(Windows:PowerShell 執行 `winget install --id astral-sh.uv -e`),然後從系統匣(工作列右下角)圖示按右鍵**結束** Claude Desktop 再重開。 diff --git a/install/windows.md b/install/windows.md index a742c38..721bd8a 100644 --- a/install/windows.md +++ b/install/windows.md @@ -2,6 +2,8 @@ 各 host 的安裝文件([Claude Desktop](claude-desktop.md)、[Cursor](cursor.md) 等)中的指令以 macOS / Linux 為例。Windows 使用者請先照本頁完成「安裝套件」與「設定 Token」,再回到對應 host 的文件貼設定;本頁最後整理了 Windows 常見問題(尤其是 host 顯示找不到 `finmind-mcp` 指令的狀況)。 +> **使用 Claude Desktop?** 直接用[一鍵安裝擴充套件](claude-desktop.md#方式一一鍵安裝擴充套件推薦):下載 `finmind.mcpb` 點兩下、貼上 token 就完成,**不需要本頁任何步驟**。本頁適用於 Cursor、Windsurf 等其他工具,或想手動設定的使用者。 + 以下指令都在 **PowerShell** 執行(開始功能表搜尋「PowerShell」開啟即可,不需系統管理員權限)。 ## 1. 安裝套件 @@ -77,7 +79,11 @@ setx FINMIND_TOKEN "your-token-here" ### host 啟動 server 失敗(找不到指令 / `ENOENT`) -最常見的 Windows 問題:`finmind-mcp` 或 `uvx` 在 PowerShell 跑得動,但 Claude Desktop 這類**圖形介面應用程式讀到的 PATH 和終端機不同**(例如安裝 uv 之前就已開著、或 PATH 是安裝程式事後才寫入的),host 啟動 MCP server 這個子行程時就找不到指令,顯示連線失敗。解法是把設定裡的 `command` 改成執行檔的**完整路徑**。 +最常見的 Windows 問題:`finmind-mcp` 或 `uvx` 在 PowerShell 跑得動,但 Claude Desktop 這類**圖形介面應用程式讀到的 PATH 和終端機不同**(例如安裝 uv 之前就已開著、或 PATH 是安裝程式事後才寫入的),host 啟動 MCP server 這個子行程時就找不到指令,顯示連線失敗。 + +**先試最簡單的**:多半只是該程式在安裝 uv 之前就開著,還沒讀到新的 PATH。把它**完全結束**(Claude Desktop 要到系統匣圖示按右鍵選結束,見下一題)再重開,通常就好了。 + +仍然不行,再把設定裡的 `command` 改成執行檔的**完整路徑**: **① 查實際位置**(在 PowerShell 執行;要打 `where.exe`,單打 `where` 在 PowerShell 是別的指令): diff --git a/mcpb/.mcpbignore b/mcpb/.mcpbignore new file mode 100644 index 0000000..f9fac8e --- /dev/null +++ b/mcpb/.mcpbignore @@ -0,0 +1,4 @@ +.venv/ +uv.lock +__pycache__/ +*.pyc diff --git a/mcpb/icon.png b/mcpb/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..76276bcf7892883c03250d231d18727fa287988e GIT binary patch literal 56784 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∨CCG!9dr6-Is0;HJ0n9GRy-%q!ZWqk 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 literal 0 HcmV?d00001 diff --git a/mcpb/manifest.json b/mcpb/manifest.json new file mode 100644 index 0000000..647dba5 --- /dev/null +++ b/mcpb/manifest.json @@ -0,0 +1,53 @@ +{ + "manifest_version": "0.4", + "name": "finmind", + "display_name": "FinMind", + "version": "0.0.0", + "description": "台股/期權/總經/全球金融資料 MCP server(FinMind)", + "long_description": "讓 Claude 直接查詢 FinMind 的台股、期貨選擇權、總體經濟與全球金融資料。安裝時輸入 FinMind Token 即可使用,不需要另外設定 Python 或編輯設定檔。", + "author": { + "name": "FinMind", + "email": "finmind.tw@gmail.com", + "url": "https://finmindtrade.com" + }, + "repository": { + "type": "git", + "url": "https://github.com/FinMind/FinMind-MCP" + }, + "homepage": "https://github.com/FinMind/FinMind-MCP", + "documentation": "https://github.com/FinMind/FinMind-MCP/blob/master/install/claude-desktop.md", + "support": "https://github.com/FinMind/FinMind-MCP/issues", + "icon": "icon.png", + "server": { + "type": "uv", + "entry_point": "src/server.py", + "mcp_config": { + "command": "uv", + "args": ["run", "--directory", "${__dirname}", "src/server.py"], + "env": { + "FINMIND_TOKEN": "${user_config.finmind_token}" + } + } + }, + "tools": [ + { "name": "query_dataset", "description": "查詢 FinMind dataset(股價、財報、籌碼、期權、總經等)" }, + { "name": "list_datasets", "description": "列出可查詢的 dataset 與所需會員等級" }, + { "name": "get_stock_info", "description": "查詢股票代號、名稱與產業別" }, + { "name": "query_trading_daily_report", "description": "查詢個股單日分點進出" }, + { "name": "get_pricing", "description": "查詢 FinMind 會員方案與價格" } + ], + "user_config": { + "finmind_token": { + "type": "string", + "title": "FinMind Token", + "description": "到 https://finmindtrade.com/analysis/#/account/user 登入,在「API Token」分頁按「複製」後貼上。", + "required": true, + "sensitive": true + } + }, + "compatibility": { + "platforms": ["darwin", "win32", "linux"] + }, + "keywords": ["finmind", "finance", "taiwan-stock", "futures", "options"], + "license": "Apache-2.0" +} diff --git a/mcpb/pyproject.toml b/mcpb/pyproject.toml new file mode 100644 index 0000000..206b66a --- /dev/null +++ b/mcpb/pyproject.toml @@ -0,0 +1,10 @@ +# Claude Desktop 擴充套件(.mcpb)用的執行環境定義。 +# Claude Desktop 啟動時以 `uv run` 依此檔自動準備 Python 與相依套件,使用者不用自己裝。 +# finmind-mcp 的版本由 scripts/build_mcpb.py 在打包時改成與發版 tag 相同,不要手動改。 +[project] +name = "finmind-mcpb" +version = "0.0.0" +requires-python = ">=3.10" +dependencies = [ + "finmind-mcp==0.0.0", +] diff --git a/mcpb/src/server.py b/mcpb/src/server.py new file mode 100644 index 0000000..9ac4837 --- /dev/null +++ b/mcpb/src/server.py @@ -0,0 +1,6 @@ +"""Claude Desktop 擴充套件(.mcpb)的進入點:直接啟動 PyPI 上的 finmind-mcp。""" + +from finmind_mcp.server import main + +if __name__ == "__main__": + main() diff --git a/scripts/build_mcpb.py b/scripts/build_mcpb.py new file mode 100644 index 0000000..837c628 --- /dev/null +++ b/scripts/build_mcpb.py @@ -0,0 +1,82 @@ +"""打包 Claude Desktop 擴充套件 dist/finmind.mcpb。 + +把 mcpb/ 複製到暫存目錄,將 manifest.json 與 pyproject.toml 的版本改成 VERSION, +再用官方 CLI(@anthropic-ai/mcpb)驗證並打包。需要 Node.js(npx)。 + +用法: + python scripts/build_mcpb.py 0.0.11 # 指定版本 + python scripts/build_mcpb.py # 沒給版本 → 讀環境變數 GITHUB_REF_NAME(發版 tag) + python scripts/build_mcpb.py --dev # CI 用:相依改指向本 repo 原始碼,測尚未發版的改動 + +finmind-mcp 會被釘在同一個版本,所以該版本必須已經在 PyPI 上 +(publish.yml 先上 PyPI 再打包,順序已保證)。 +""" + +from __future__ import annotations + +import json +import os +import re +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SRC = ROOT / "mcpb" +OUT = ROOT / "dist" / "finmind.mcpb" +MCPB_CLI = "@anthropic-ai/mcpb@2" +VERSION_RE = re.compile(r"^\d+\.\d+\.\d+$") + + +def resolve_version(args: list[str]) -> str: + raw = args[0] if args else os.environ.get("GITHUB_REF_NAME", "") + version = raw.removeprefix("v") + if not VERSION_RE.match(version): + sys.exit(f"版本需為 X.Y.Z(可帶 v 前綴),收到:{raw!r}") + return version + + +def stamp(build_dir: Path, version: str, requirement: str) -> None: + manifest_path = build_dir / "manifest.json" + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + manifest["version"] = version + manifest_path.write_text( + json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" + ) + + pyproject_path = build_dir / "pyproject.toml" + text = pyproject_path.read_text(encoding="utf-8") + text, n_pin = re.subn(r'"finmind-mcp==[^"]*"', f'"{requirement}"', text) + text, n_ver = re.subn(r'(?m)^version = "[^"]*"$', f'version = "{version}"', text) + if n_pin != 1 or n_ver != 1: + sys.exit("mcpb/pyproject.toml 格式不符,找不到 finmind-mcp 釘版或 version 欄位") + pyproject_path.write_text(text, encoding="utf-8") + + +def main() -> None: + args = sys.argv[1:] + dev = "--dev" in args + if dev: + version, requirement = "0.0.0", f"finmind-mcp @ {ROOT.as_uri()}" + else: + version = resolve_version(args) + requirement = f"finmind-mcp=={version}" + npx = shutil.which("npx") + if npx is None: + sys.exit("找不到 npx,請先安裝 Node.js") + + with tempfile.TemporaryDirectory() as tmp: + build_dir = Path(tmp) / "finmind" + shutil.copytree(SRC, build_dir, ignore=shutil.ignore_patterns(".venv", "uv.lock", "__pycache__")) + stamp(build_dir, version, requirement) + OUT.parent.mkdir(exist_ok=True) + subprocess.run([npx, "-y", MCPB_CLI, "validate", str(build_dir)], check=True) + subprocess.run([npx, "-y", MCPB_CLI, "pack", str(build_dir), str(OUT)], check=True) + + print(f"built {OUT.relative_to(ROOT)} ({requirement})") + + +if __name__ == "__main__": + main() diff --git a/scripts/check_mcpb.py b/scripts/check_mcpb.py new file mode 100644 index 0000000..937731d --- /dev/null +++ b/scripts/check_mcpb.py @@ -0,0 +1,100 @@ +"""模擬 Claude Desktop 安裝並啟動 dist/finmind.mcpb,確認擴充套件真的能用。 + +步驟與 host 相同:解壓縮 → 讀 manifest 的 mcp_config → 代入 ${__dirname} 與 +${user_config.*} → 啟動子行程 → 走 MCP 握手 → tools/list → 呼叫一個離線 tool。 +需要 PATH 上有 uv(Claude Desktop 啟動 uv 類型擴充套件時同樣會用 uv)。 + +用法: + python scripts/check_mcpb.py [dist/finmind.mcpb] +""" + +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import sys +import tempfile +import zipfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +DEFAULT_BUNDLE = ROOT / "dist" / "finmind.mcpb" +FAKE_TOKEN = "mcpb-check" + + +def substitute(value: str, dirname: Path, user_config: dict[str, str]) -> str: + value = value.replace("${__dirname}", str(dirname)) + for key, val in user_config.items(): + value = value.replace("${user_config.%s}" % key, val) + return value + + +def main() -> None: + bundle = Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_BUNDLE + with tempfile.TemporaryDirectory() as tmp: + ext_dir = Path(tmp) / "finmind" + with zipfile.ZipFile(bundle) as zf: + zf.extractall(ext_dir) + + manifest = json.loads((ext_dir / "manifest.json").read_text(encoding="utf-8")) + assert manifest["server"]["type"] == "uv", manifest["server"] + user_config = {key: FAKE_TOKEN for key in manifest.get("user_config", {})} + cfg = manifest["server"]["mcp_config"] + command = shutil.which(cfg["command"]) + if command is None: + sys.exit(f"找不到 {cfg['command']},請先安裝 uv") + args = [substitute(a, ext_dir, user_config) for a in cfg.get("args", [])] + env = {k: substitute(v, ext_dir, user_config) for k, v in cfg.get("env", {}).items()} + assert env.get("FINMIND_TOKEN") == FAKE_TOKEN, "token 沒有從 user_config 傳進 env" + + proc = subprocess.Popen( + [command, *args], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + text=True, encoding="utf-8", env={**os.environ, **env}, + ) + assert proc.stdin is not None and proc.stdout is not None + + def send(msg: dict) -> None: + proc.stdin.write(json.dumps(msg) + "\n") + proc.stdin.flush() + + def request(msg: dict) -> dict: + # 跟真正的 host 一樣保持 stdin 開著,一問一答;略過 server 主動送的通知。 + send(msg) + while True: + line = proc.stdout.readline() + if not line: + proc.kill() + sys.exit(f"server 提早結束(等待 {msg['method']})\nstderr:\n{proc.stderr.read()}") + resp = json.loads(line) + if resp.get("id") == msg["id"]: + return resp + + try: + init = request({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { + "protocolVersion": "2024-11-05", "capabilities": {}, + "clientInfo": {"name": "check_mcpb", "version": "0"}}}) + assert "result" in init, init + send({"jsonrpc": "2.0", "method": "notifications/initialized"}) + tools = request({"jsonrpc": "2.0", "id": 2, "method": "tools/list"}) + call = request({"jsonrpc": "2.0", "id": 3, "method": "tools/call", + "params": {"name": "list_datasets", "arguments": {}}}) + finally: + proc.stdin.close() + try: + proc.wait(timeout=10) + except subprocess.TimeoutExpired: + proc.kill() + + tool_names = sorted(t["name"] for t in tools["result"]["tools"]) + declared = sorted(t["name"] for t in manifest.get("tools", [])) + assert tool_names == declared, f"manifest tools {declared} != server tools {tool_names}" + result = call["result"] + assert not result.get("isError") and result["content"], call + print(f"OK finmind.mcpb {manifest['version']}: {len(tool_names)} tools {tool_names}") + + +if __name__ == "__main__": + main() diff --git a/tests/test_mcpb.py b/tests/test_mcpb.py new file mode 100644 index 0000000..3e25ae5 --- /dev/null +++ b/tests/test_mcpb.py @@ -0,0 +1,68 @@ +"""Tests for the Claude Desktop extension bundle (mcpb/) and its build script.""" + +import importlib.util +import json +from pathlib import Path + +import pytest + +from finmind_mcp import tools + +ROOT = Path(__file__).resolve().parent.parent +MANIFEST = json.loads((ROOT / "mcpb" / "manifest.json").read_text(encoding="utf-8")) + + +def _load_build_script(): + spec = importlib.util.spec_from_file_location("build_mcpb", ROOT / "scripts" / "build_mcpb.py") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_manifest_tools_match_server(): + # Claude Desktop 安裝畫面顯示的是 manifest 的 tools;新增 tool 時要一起補。 + declared = sorted(t["name"] for t in MANIFEST["tools"]) + assert declared == sorted(t.name for t in tools.tool_definitions()) + + +def test_manifest_uses_uv_runtime_and_passes_token(): + server = MANIFEST["server"] + assert server["type"] == "uv" + assert (ROOT / "mcpb" / server["entry_point"]).is_file() + assert server["mcp_config"]["env"]["FINMIND_TOKEN"] == "${user_config.finmind_token}" + token = MANIFEST["user_config"]["finmind_token"] + assert token["required"] is True and token["sensitive"] is True + + +def test_manifest_does_not_require_system_python(): + # 宣告 runtimes.python 會讓 Claude Desktop 檢查系統 Python,沒裝就拒絕安裝; + # uv 類型由 host 自己準備 Python,不該宣告。 + assert "runtimes" not in MANIFEST.get("compatibility", {}) + + +def test_stamp_pins_release_version(tmp_path): + build = _load_build_script() + for name in ("manifest.json", "pyproject.toml"): + (tmp_path / name).write_text((ROOT / "mcpb" / name).read_text(encoding="utf-8"), encoding="utf-8") + + build.stamp(tmp_path, "1.2.3", "finmind-mcp==1.2.3") + + assert json.loads((tmp_path / "manifest.json").read_text(encoding="utf-8"))["version"] == "1.2.3" + pyproject = (tmp_path / "pyproject.toml").read_text(encoding="utf-8") + assert '"finmind-mcp==1.2.3"' in pyproject + assert 'version = "1.2.3"' in pyproject + + +@pytest.mark.parametrize( + "args, env, expected", + [(["0.0.11"], "", "0.0.11"), ([], "v0.1.0", "0.1.0"), ([], "0.2.0", "0.2.0")], +) +def test_resolve_version(monkeypatch, args, env, expected): + monkeypatch.setenv("GITHUB_REF_NAME", env) + assert _load_build_script().resolve_version(args) == expected + + +def test_resolve_version_rejects_non_release(monkeypatch): + monkeypatch.setenv("GITHUB_REF_NAME", "master") + with pytest.raises(SystemExit): + _load_build_script().resolve_version([])