# AGENTS.md 台股資料 MCP server。給任何 AI 代理讀的專案慣例; 使用說明看 [README.md](README.md),詞彙定義看 [CONTEXT.md](CONTEXT.md), 決策與實測紀錄看 [docs/spec-workers-migration.md](docs/spec-workers-migration.md), 架構決策看 [docs/adr/](docs/adr/)——那裡有活的操作約束(例如導入認證前必須重新評估 `cacheScope`),不是純歷史。 ## 更新紀錄 使用者看得到的改動,合併時寫進 `CHANGELOG.md` 與 `CHANGELOG.en.md` 的「尚未編號」一節(沒有就在最新版本上面新增); 升版時把那一節改成版本號與日期。只寫對使用者的影響,不寫內部重構。 `npm run check-readmes` 會擋下 `package.json` 版本在兩份 CHANGELOG 裡沒有對應一節的情況。 ## 分支保護 `main` 要 PR 才能合併,required check 只有 `test`(`test.yml` 的 job)。設定在 GitHub,現況用 `gh api repos/taux-io/twse-mcp/branches/main/protection` 查。 - `upstream health` 的 job 叫 `check`,只在排程時跑;把它加進 required,每個 PR 都會永遠卡住。 - 改 `test` 這個 job 的名稱、或給它的 `pull_request` 觸發加 `paths` 篩選,PR 一樣會卡住。 `scripts/check-required-checks.mjs` 在 CI 擋這兩件事;改了 GitHub 上的設定要同步它的 `REQUIRED_CHECKS`, 再跑 `npm run check-required-checks -- --remote` 對一次。 ## 發版 發版是推一個 `v*` tag,`publish-mcp.yml` 會把 `server.json` 發布到官方 MCP Registry。 `v*` tag 受 repo ruleset `v* release tags` 保護:建立、更新、刪除都被拒絕,沒有任何人可以繞過,admin 也一樣。 所以打 tag 之前要先停用它,推完立刻啟用。停用與啟用都會留在 repo 的稽核紀錄裡,這是刻意的: `v*` tag 一推就觸發發布流程,不該有人能安靜地建立或移動它。 1. 升版的 PR 先合併:`package.json`、`package-lock.json`、`server.json` 的版本,以及兩份 CHANGELOG 的版本號與日期(見「更新紀錄」)。 2. 停用 ruleset、在升版的那個 commit 上打 tag 並推送、啟用 ruleset: ```sh RULESET=repos/taux-io/twse-mcp/rulesets/27170715 gh api -X PUT "$RULESET" -f enforcement=disabled -q .enforcement # 印出 disabled git tag v0.16.0 origin/main git push origin v0.16.0 gh api -X PUT "$RULESET" -f enforcement=active -q .enforcement # 印出 active ``` 3. 到 Actions 核准 `mcp-registry` environment 的部署。它只接受 `v*` tag,從 `main` 手動觸發會被拒絕。 4. 確認 ruleset 是啟用的:`gh api repos/taux-io/twse-mcp/rulesets -q '.[] | "\(.id) \(.name) \(.enforcement)"'`。 - tag 推錯了要移動或刪除,一樣要先停用 ruleset。 - ruleset 的 id 變了(被刪掉重建)就用第 4 步的指令查新的 id。 - 這道保護只管 tag。有 write 權限的人仍然可以另外寫一支 workflow 拿到同一張 registry 憑證, 那一側靠 `smoke.yml` 每天比對 registry 條目(`scripts/check-registry.mjs`)來偵測。 ## 休市日副本 證交所的休市日表只列一個年度,而且年底前就換成下一年度(2026-11-13 觀察到已是 2027 年)。 `src/holidays.generated.json` 是本服務保存的副本,一個年度一份。執行期以上游為準,上游不涵蓋的年度才用副本, 並在說明裡交代依據是副本。`dataset.get` 查休市日表仍然回上游原文。 - `npm run refresh-holidays` 從 OpenAPI 把上游目前列的那一年併進副本,從不刪掉年度。每週的目錄刷新 (`refresh-catalog.yml`)會跑它;副本有變動時,與目錄一起推到機器人分支並開 issue。 - 2026 年那一份取自證交所網站的查詢(當時 OpenAPI 已經不列 2026 年),來源寫在檔案裡。 ## 依賴升級 Dependabot 的 PR 都改 `package-lock.json`,合併一個後其他的會衝突:留言 `@dependabot rebase`,一次重建一個。 `agents` 升版的 PR 要手動把 `@modelcontextprotocol/server` 一起升:`agents` 把它釘在一個精確的 peer 版本, 而 Dependabot 被設定成不開 `server` 的 PR(`.github/dependabot.yml`)。先看新版 `agents` 要哪一版 (`npm view agents@<新版> peerDependencies`),把 `package.json` 的 `server` 改成同一版,再 `npm install`。 三個正式相依(`server`、`agents`、`zod`)都用精確版本,升版只走 PR。 `smoke` 在 push 到 `main` 時不跑,合併後要驗就 `gh workflow run smoke.yml`。 驗證做法與新舊回應比對的結果見 [docs/dependency-upgrade-2026-10-17.md](docs/dependency-upgrade-2026-10-17.md)。 ## Agent skills ### Issue tracker Issues 存放在 GitHub(`taux-io/twse-mcp`),以 `gh` CLI 操作。見 `docs/agents/issue-tracker.md`。 ### Triage labels 沿用五個標準標籤,字串與角色同名。見 `docs/agents/triage-labels.md`。 ### Domain docs Single-context:根目錄一份 `CONTEXT.md`,ADR 放 `docs/adr/`。見 `docs/agents/domain.md`。 ### Ponytail checkpoints commit 前、接手或重構前、每週或階段結束,各跑一個 ponytail 過度工程檢查。 時機、標記慣例,以及這個 repo 的防禦性守衛為什麼不算冗餘,見 `docs/agents/ponytail.md`。 ### Answer eval `npm run eval:answers` 讓 Claude Code 真的查完、作答,再用規則核對答案(`scripts/eval-answers.mjs`)。`eval:tools` 只看第一個工具呼叫,看不到多步驟的後續(先查代號,再帶對參數),也看不到模型有沒有照回應裡的說明讀數字 (單位、累計數、N/A 不是 0)。改到 caveats、`note`、單位說明,或回應裡任何給模型讀的解說文字時,兩個模型各跑一次 (`EVAL_REPEAT=3`)。標準答案取自同一次對話的工具結果;每題是通過、失敗、需人工看、沒驗到四種之一——「沒驗到」 不算通過。「說它沒有」這類反向句用規則寫不完(否定、疑問、引用別家),命中只標「需人工看」,要讀原文判斷。 只打本機、需要登入的 Claude Code,所以不進 CI。 ### Smoke test `npm run smoke` 對本機 `wrangler dev`(8787)的每支工具發一個真實查詢,兩代協定各一遍;8787 沒人用就自己起、跑完關掉。改到工具回應的形狀或 `outputSchema` 時在本機跑一次;CI(`.github/workflows/smoke.yml`)在每個 PR 與每天定時跑,定時的失敗會開 issue。 預設不打正式環境:測試請求會混進真實流量,付費方案也依用量計費。(#104 當時跑在 Workers 免費方案,每個請求都在消耗 10ms CPU 的寬容額度;2026-10-31 已升級付費。) 例外是每天排程裡的正式環境存活檢查(`SMOKE_LIST_ONLY=1`):看工具清單、快捷指令與首頁,外加 modern 的 `dataset.get` 一次(證明工具真的查得到資料),其餘工具不呼叫。 排程同時跑 `npm run check-registry`:比對官方 MCP Registry 上的條目與 `server.json` 的 `remotes`,不一致就失敗並開 issue(條目被改指到別的主機時的偵測)。 ### 查正式環境 打正式環境的工具呼叫用 `npm run call -- <工具> '' --prod`(不加 `--prod` 打本機 8787); 查正式 D1 的筆數與日期、看排程 log,見 `docs/adr/0003-d1-daily-archive.md` 的「維運查詢」。 ### 介面契約快照 工具名稱、參數名稱與型別、必填、enum,以及快捷指令的參數,存在 `test/__snapshots__/server.test.ts.snap`。 改到它們測試就會失敗——這是使用者的設定(允許清單、寫死的呼叫)依賴的契約。確定是刻意的破壞性改動, 才用 `npx vitest -u` 更新快照,並在 CHANGELOG 寫清楚使用者要怎麼改。說明文字不在快照裡。 ### Tool-selection eval 改工具描述、`instructions` 或新增工具之前與之後,各跑一次 `npm run eval:tools`,比較通過題數。 預設用本機的 Claude Code(`claude -p`,走訂閱額度,不產生 API 帳單);至少再用 `EVAL_MODEL=sonnet` 跑一次—— 較小的模型比較抓得到描述寫得不夠清楚的地方;每次的分數記在該 PR 的 eval 表格裡。 題目在 `evals/tool-selection.json`,只看第一個工具呼叫;測本機未部署的描述用 `EVAL_ENDPOINT=http://localhost:8787/mcp`。 用法見 `scripts/eval-tools.mjs` 開頭;PR 範本(`.github/pull_request_template.md`)有對應的欄位要填。 **單跑一次不能判定退步。** 模型本身有隨機性:2026-09-26 實測 sonnet 對「殖利率前 10 檔」 重跑 8 次只過 6 次。改動後出現失敗題,用 `EVAL_ONLY=<題目> EVAL_REPEAT=10 npm run eval:ab` 在兩個本機伺服器上**交錯**重跑再比較 (做法見「要比就在同一段時間、同一種環境比」那段),不要拿正式環境對本機比。 描述裡點名其他工具時,指向 `dataset.search`,不要指向 `dataset.get`: 點名取資料的工具,會讓較小的模型跳過搜尋、直接猜 dataset_id。 **要比就在同一段時間、同一種環境比。** 2026-09-26 加股利時,「殖利率前 10 檔」先是正式環境 10/11、本機 6/11, 像是退步;但本機換回與正式環境逐字相同的工具清單也只有 5/8。最後用 `git worktree` 在 8788 起一個改動前的 `wrangler dev`,與改動後的 8787 **交錯**各跑 10 次,得到 9/10 對 8/10——落差來自時段,不是改動。 所以改動前後的比較一律用 `npm run eval:ab`:它把 main(或 `EVAL_BASE`)放進暫時的 worktree 起在 8788、 工作目錄起在 8787,每題兩邊交錯問,輸出每題的對照,結束時自動清掉。懷疑某題退步時用 `EVAL_ONLY=<題目> EVAL_REPEAT=10 npm run eval:ab`。另一個理由是 eval 的大量請求會混進真實流量,並產生用量費用。 (#104 當時正式環境在 Workers 免費方案,會吃掉每個請求 10ms CPU 的寬容額度;2026-10-31 已升級付費。)