Skip to main content
Glama
supermanko1102

mol-open-data-mcp

mol-open-data-mcp

勞動部開放資料 MCP Server — 部署在 Cloudflare Workers 的 remote MCP, 讓任何 MCP client(Claude、ChatGPT、Cursor…)可以查詢:

  • 違反勞動法令事業單位名單(勞基法 109896、就服法 110908)— 支援公司名稱模糊搜尋

  • 勞動部所有開放資料集的搜尋、詮釋資料、以及 datastore 內容查詢

上游 API:勞動部 OAS 標準 Open API(https://apiservice.mol.gov.tw/OdService)。

架構

MCP client ── /mcp ──> Workers (McpAgent, Durable Object)
                          │
                          ├─ 即時 proxy ──> 勞動部 OdService API
                          │                 (find_datasets / get_dataset_info / query_data)
                          │
                          └─ D1 (SQLite) <── 每日 cron 增量同步
                                            + FTS5 trigram 索引(search_violations 模糊比對用)

為什麼要 D1 快取:勞動部 datastore API 的 filters 只支援精確比對, 「查鼎泰豐」對不到「鼎泰豐小吃店股份有限公司」。所以把名單同步進 D1, 存一份正規化名稱(去空白/去公司後綴/全形轉半形),查詢時用同一套正規化比對。

為什麼是增量而不是每天全量重建:勞基法名單實測有 76,919 筆。 每天砍掉重建等於一天 15 萬次以上的 rows written,直接爆掉 Workers 免費版的 100,000 rows written/day;77 頁分頁也會撞上「每次調用 50 次 subrequest」的上限。 實際上這份名單是累積型的,每天只新增約 26 筆,所以改成:

  • 初次灌檔走離線 seed(scripts/seed.mjs),跑在你自己的機器上,不受 Worker 配額限制

  • 之後每日 cron 只從上次掃到的位置往後掃,INSERT OR IGNORE 去重

Related MCP server: Taiwan Tender MCP

上游 API 的坑(實測結論,改程式前請先看這段)

這些行為與官方 OAS 文件不符,是 2026-07 實際打 API 量出來的:

現象

實測

影響

result.total

永遠不回傳

不能用 total 判斷分頁結束,只能掃到空頁為止

result.fields

恆為空陣列

拿不到欄位 schema

sort 降冪

欄位 desc-欄位欄位:desc 全部被拒

只能升冪,所以「拿最新資料」= 從尾端 offset 往後掃

不帶 sort 分頁

順序不穩定,第 1 頁與第 2 頁重疊 390/1000 筆

分頁一律要帶 sort,否則會同時漏抓又重複抓

sort 分頁

仍有少量重疊(同日期 tie-break 不穩,約 13/1000)

還是要靠 dedup_key 去重

一個資料集多組資源

109896 有 8 個 distribution(2 組 × 4 格式),兩個 JSON 的 resourceModified 差兩個月

必須取最新的那個,不能取第一個

resourceAmount

字串 "0",不可信

不能拿來當筆數

資料量(2026-07 實測):勞基法 76,919 筆、就服法 84 筆。 勞基法的罰鍰金額只有 45,912 筆(60%)有值。

首次部署

npm install
npx wrangler login

# 1. 建 D1 資料庫,把回傳的 database_id 填入 wrangler.jsonc
npx wrangler d1 create mol_mcp_db

# 2. 建表(含 FTS5 索引與觸發器)
npm run db:init

# 3. 產生初始資料並灌進去(在本機跑,不受 Worker 配額限制)
npm run seed:build      # 抓上游 → 產生 seed.sql,約 1 分鐘
npm run seed:remote     # wrangler d1 execute --remote --file=./seed.sql

# 4. 設定 /admin/sync 的驗證 token(沒設的話該端點直接回 404)
npx wrangler secret put ADMIN_TOKEN

# 5. 部署
npx wrangler deploy

之後每天 UTC 19:30(台灣 03:30)cron 會自動增量同步,不需要再手動灌。

MCP endpoint:https://mol-open-data-mcp.<你的帳號>.workers.dev/mcp

資料範圍

wrangler.jsoncSYNC_SINCE_YEAR 控制只保留公告年份 >= 此值的紀錄, 預設 "2024"(約 22,900 筆)。設 "0" 表示全收。

改這個值時要同步改 seed 的 --since(在 package.jsonseed:build), 否則 seed 灌的範圍和每日同步的範圍會不一致。

年份與累計筆數(實測):

起始年

累計筆數

2026

5,551

2025

14,950

2024(預設)

22,975

2020

49,234

全量

76,919

全量 76,919 筆一次灌會逼近 D1 免費版 100,000 rows written/day(主表 + FTS 索引都算), 要全量請分兩天灌,或升級 Workers Paid。

本機開發

npm run db:init:local
npm run seed:build && npm run seed:local
npm run dev            # endpoint 在 http://localhost:8787/mcp

手動觸發一次增量同步(本機 cron 不會自動跑):

curl "http://localhost:8787/cdn-cgi/handler/scheduled"

也可用 MCP Inspector 測試: npx @modelcontextprotocol/inspector

在 Claude 使用

Claude.ai → Settings → Connectors → Add custom connector → 貼上 /mcp URL。 (Claude Code:claude mcp add --transport http mol https://.../mcp)

MCP 工具

工具

用途

search_violations

公司/雇主違規紀錄模糊查詢(D1 快取 + FTS5)

find_datasets

依標籤或分類找資料集

get_dataset_info

資料集詮釋資料(resourceID、欄位 schema)

query_data

查任一資源內容(filters/fields/sort/分頁)

data_freshness

快取同步時間與筆數

模糊搜尋怎麼做的

name_norm 上的一般 B-tree 索引對 LIKE '%x%'(前置萬用字元)完全無效, 會變成每次查詢全表掃描。所以改用 FTS5 trigram 分詞器 —— 中文沒有空格, 只有 trigram 能做子字串比對。

trigram 的限制是至少要 3 個字(實測:鼎泰 → 0 筆,鼎泰豐 → 命中), 所以正規化後長度 < 3 的查詢改走前綴範圍比對,代價是只能比對名稱開頭 (查「王品」找得到「王品餐飲」,但找不到「台灣王品」)。工具回應會明講 本次用的是前綴比對,不讓模型把不完整的結果當成完整答案。

這裡有個容易踩的坑:前綴比對不能用 LIKE 'x%'。SQLite 的 LIKE 前綴 優化要求索引是 NOCASE collation,而預設建出來的索引是 BINARY, 所以 LIKE 'x%' 仍然全表掃描。要改成範圍比較才吃得到索引:

-- 全表 SCAN,讀 22,864 列
WHERE name_norm LIKE '王品%'
-- SEARCH USING INDEX,讀 15 列
WHERE name_norm >= '王品' AND name_norm < '王品' || char(1114111)

三條路徑的實測成本:

查詢方式

rows_read

免費版 5M/day 可查次數

FTS5 trigram(≥3 字)

51

~98,000

前綴範圍(<3 字)

15

~333,000

LIKE '%x%' 全掃

22,864

218

已知漏配:查「台積電」對不到「台灣積體電路製造股份有限公司」—— 俗名與登記名稱的落差沒有解,要靠商工登記資料才能對起來(見下方 TODO)。

上線前 TODO

  • /admin/sync 加驗證 已改成需要 ADMIN_TOKEN,未設定則回 404

  • 加 rate limit /mcp 已加上依 IP 的 100 次/60 秒限制(見下方限制說明)

  • 若要擋分散式濫用,需要 WAF 規則或 Turnstile —— Rate Limiting binding 擋不住

  • 觀察各縣市欄位名稱差異,補 sync.tsFIELD_ALIASES

  • 加入其他法規名單(性平法等):在 sync.tsVIOLATION_DATASETS 加一行

  • 名稱比對進階:接經濟部商工登記把名稱對回統編,處理俗名/分公司/更名

  • 上游若下架舊紀錄,增量同步不會察覺(全量重建才會)。目前觀察是累積型 (2011 年的紀錄仍在),若之後發現會下架,需要定期重跑 seed 對帳

  • McpServer.tool() 在目前 SDK 版本已標為 deprecated,可改用 registerTool()

  • 考慮改用 Cloudflare 新的 stateless mcp-worker 範本

流量限制(以及它擋不住什麼)

/mcp 依來源 IP 限制 100 次/60 秒(Workers Rate Limiting binding, 設定在 wrangler.jsoncratelimits)。超過回 HTTP 429 + JSON-RPC error code -32029

實測結果要說清楚:

測試方式

結果

單一 keep-alive 連線、循序 150 次

✅ 第 ~100 次後開始擋

50 條並行連線、200 次 / 46 秒

❌ 全數放行

原因是計數器綁在各個 Cloudflare 節點/isolate 上,並行連線會被分散到不同 isolate、各自計數;官方文件也明講此機制是 permissive、最終一致的。 所以它是安全閥,不是配額控制。 服務真正撐得住是靠把查詢做便宜 (見上方三條路徑的 rows_read 對照),而不是靠這道限流。

要擋真正的分散式濫用,得用 WAF 規則或 Turnstile。

資料誠實性原則

  • 罰鍰金額:109/6/12 勞基法 §80-1 修正前的處分未必公布金額(實測 40% 的紀錄無金額), 查無金額時工具會明講,不讓模型自行腦補。

  • 查無紀錄 ≠ 從未違規:名單揭露範圍與期間受法規限制,且本服務預設只存近年資料, 回覆中一律附註。

  • query_data 會告知「上游不提供總筆數」以及分頁需帶 sort,不讓模型誤以為 回傳的就是全部。

  • data_freshness 讓使用者隨時可查快取新鮮度與實際筆數。

License

資料來源:政府資料開放授權條款(OGDL 1.0,相容 CC BY 4.0)。 程式碼:MIT(自行調整)。

Related MCP Connectors

Related MCP Servers