mol-open-data-mcp
Enables querying Taiwan's Ministry of Labor open data, including lists of companies violating labor laws with fuzzy search, and searching/querying ministry datasets.
README
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去重
上游 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.jsonc 的 SYNC_SINCE_YEAR 控制只保留公告年份 >= 此值的紀錄,
預設 "2024"(約 22,900 筆)。設 "0" 表示全收。
改這個值時要同步改 seed 的 --since(在 package.json 的 seed: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%' 全掃 |
已知漏配:查「台積電」對不到「台灣積體電路製造股份有限公司」—— 俗名與登記名稱的落差沒有解,要靠商工登記資料才能對起來(見下方 TODO)。
上線前 TODO
- [x]
已改成需要/admin/sync加驗證ADMIN_TOKEN,未設定則回 404 - [x]
加 rate limit/mcp已加上依 IP 的 100 次/60 秒限制(見下方限制說明) - [ ] 若要擋分散式濫用,需要 WAF 規則或 Turnstile —— Rate Limiting binding 擋不住
- [ ] 觀察各縣市欄位名稱差異,補
sync.ts的FIELD_ALIASES - [ ] 加入其他法規名單(性平法等):在
sync.ts的VIOLATION_DATASETS加一行 - [ ] 名稱比對進階:接經濟部商工登記把名稱對回統編,處理俗名/分公司/更名
- [ ] 上游若下架舊紀錄,增量同步不會察覺(全量重建才會)。目前觀察是累積型 (2011 年的紀錄仍在),若之後發現會下架,需要定期重跑 seed 對帳
- [ ]
McpServer.tool()在目前 SDK 版本已標為 deprecated,可改用registerTool() - [ ] 考慮改用 Cloudflare 新的 stateless mcp-worker 範本
流量限制(以及它擋不住什麼)
/mcp 依來源 IP 限制 100 次/60 秒(Workers Rate Limiting binding,
設定在 wrangler.jsonc 的 ratelimits)。超過回 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(自行調整)。
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。