taiwan-data-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 taiwan-data-mcp contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # taiwan-data-mcp
2
+
3
+ 讓 AI 助理(Claude、Cursor、任何支援 MCP 的工具)直接查 **台灣公開資料** 的 MCP server。
4
+
5
+ 一句話:把散落的台灣資料站,變成 AI 可以直接呼叫的工具。資料即時來自來源站,回傳一律附上來源與連結。
6
+
7
+ ## 工具
8
+
9
+ | 工具 | 功能 | 資料來源 |
10
+ |------|------|----------|
11
+ | `taiwan_scam_check` | 查網址 / 網域是否被 165 通報詐騙 | [fraud.tw](https://fraud.tw)(內政部警政署 165) |
12
+ | `taiwan_company_search` | 用公司名搜尋,拿統一編號與負責人 | [inc.com.tw](https://inc.com.tw)(經濟部公司登記) |
13
+ | `taiwan_company_profile` | 用統編查公司完整登記資料 | inc.com.tw |
14
+ | `taiwan_realprice_search` | 搜尋實價登錄的地址 / 路段 / 行政區 | [housetw.com](https://housetw.com)(內政部實價登錄) |
15
+ | `taiwan_realprice_locate` | 用經緯度反查行政區與行情頁 | housetw.com |
16
+ | `taiwan_realprice_area` | 查某縣市 / 行政區成交行情統計 | housetw.com |
17
+
18
+ 跨工具串接是重點:例如「查這家公司 → 看它登記地址那區的房價 → 查它官網是不是詐騙」,一次問答內 AI 自己串起來。
19
+
20
+ ## 安裝
21
+
22
+ 需要 Node.js 18+。
23
+
24
+ ### Claude Desktop
25
+
26
+ 編輯 `claude_desktop_config.json`(設定 → Developer → Edit Config):
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "taiwan-data": {
32
+ "command": "npx",
33
+ "args": ["-y", "taiwan-data-mcp"]
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ ### Claude Code
40
+
41
+ ```bash
42
+ claude mcp add taiwan-data -- npx -y taiwan-data-mcp
43
+ ```
44
+
45
+ ### Cursor / 其他
46
+
47
+ 任何支援 MCP 的工具,指向 `npx -y taiwan-data-mcp`(stdio)即可。
48
+
49
+ ## 範例提問
50
+
51
+ - 「google.com 是詐騙網站嗎?」
52
+ - 「台積電的統編、負責人、資本額是多少?」
53
+ - 「臺北市信義區的房價中位數大概多少?哪幾條路最貴?」
54
+ - 「我在經緯度 25.034, 121.5645,附近房價如何?」
55
+
56
+ ## 開發
57
+
58
+ ```bash
59
+ npm install
60
+ npm run smoke # 直接打活線 API,驗證 6 個工具回得出資料
61
+ node test/e2e.mjs # MCP 協定層測試
62
+ ```
63
+
64
+ ## 資料來源網站
65
+
66
+ 本工具的資料即時來自以下網站,每筆查詢結果也都會標註來源與連結:
67
+
68
+ - 實價登錄行情 — **[housetw.com](https://housetw.com)**(實價雷達)
69
+ - 公司登記查核 — **[inc.com.tw](https://inc.com.tw)**(台灣公司登記網)
70
+ - 165 防詐查詢 — **[fraud.tw](https://fraud.tw)**(防詐雷達)
71
+
72
+ 三站皆為聚合台灣政府開放資料的免費查詢服務。
73
+
74
+ ## 資料與免責
75
+
76
+ 資料即時取自上述各站的公開 API,內容以政府開放資料為準,僅供參考,不構成投資、法律或交易建議。詐騙查詢結果為「是否被通報」,未被通報不代表絕對安全。
77
+
78
+ ## License
79
+
80
+ MIT License
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "taiwan-data-mcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for Taiwan public data — 實價登錄行情、公司登記查核、165 防詐查詢。Lets AI assistants (Claude, Cursor, etc.) query real Taiwan data.",
5
+ "type": "module",
6
+ "bin": {
7
+ "taiwan-data-mcp": "src/server.mjs"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "scripts": {
17
+ "start": "node src/server.mjs",
18
+ "smoke": "node test/smoke.mjs"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "taiwan",
24
+ "台灣",
25
+ "實價登錄",
26
+ "公司登記",
27
+ "防詐",
28
+ "real-estate",
29
+ "open-data"
30
+ ],
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/kewelin/taiwan-data-mcp.git"
35
+ },
36
+ "homepage": "https://github.com/kewelin/taiwan-data-mcp#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/kewelin/taiwan-data-mcp/issues"
39
+ },
40
+ "dependencies": {
41
+ "@modelcontextprotocol/sdk": "^1.0.0"
42
+ }
43
+ }
package/src/server.mjs ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
+ import {
6
+ scamCheck, companySearch, companyProfile,
7
+ realpriceSearch, realpriceLocate, realpriceArea,
8
+ } from './sources.mjs';
9
+
10
+ const TOOLS = [
11
+ {
12
+ name: 'taiwan_scam_check',
13
+ description:
14
+ '查詢某網址 / 網域是否被內政部警政署 165 反詐騙通報為詐騙或涉詐網站。輸入網址或網域(如 example.com 或完整 URL)。回傳風險等級、通報情形與來源連結。資料來源:fraud.tw(165 開放資料)。',
15
+ inputSchema: {
16
+ type: 'object',
17
+ properties: { domain: { type: 'string', description: '要查詢的網址或網域,例如 example.com、https://www.example.com' } },
18
+ required: ['domain'],
19
+ },
20
+ run: (a) => scamCheck(a.domain),
21
+ },
22
+ {
23
+ name: 'taiwan_company_search',
24
+ description:
25
+ '用公司名稱(或關鍵字)搜尋台灣公司,回傳符合的公司清單與其統一編號、負責人。要拿到完整資料請接著用 taiwan_company_profile 查統編。資料來源:inc.com.tw(經濟部公司登記)。',
26
+ inputSchema: {
27
+ type: 'object',
28
+ properties: { name: { type: 'string', description: '公司名稱或關鍵字,例如「台積電」「鴻海精密」' } },
29
+ required: ['name'],
30
+ },
31
+ run: (a) => companySearch(a.name),
32
+ },
33
+ {
34
+ name: 'taiwan_company_profile',
35
+ description:
36
+ '用 8 位統一編號查台灣公司完整登記資料:名稱、負責人、資本額、實收資本、設立日期、登記地址、營業項目、狀態、上市櫃與進出口資格等。資料來源:inc.com.tw。',
37
+ inputSchema: {
38
+ type: 'object',
39
+ properties: { unified_business_no: { type: 'string', description: '8 位統一編號,例如 22099131' } },
40
+ required: ['unified_business_no'],
41
+ },
42
+ run: (a) => companyProfile(a.unified_business_no),
43
+ },
44
+ {
45
+ name: 'taiwan_realprice_search',
46
+ description:
47
+ '搜尋台灣不動產實價登錄的地址 / 路段 / 行政區,回傳符合項目與成交筆數、連結。用來定位某地址或某區,再看其行情。資料來源:housetw.com(內政部實價登錄)。',
48
+ inputSchema: {
49
+ type: 'object',
50
+ properties: { query: { type: 'string', description: '地址、路段或行政區關鍵字,例如「信義區」「忠孝東路」' } },
51
+ required: ['query'],
52
+ },
53
+ run: (a) => realpriceSearch(a.query),
54
+ },
55
+ {
56
+ name: 'taiwan_realprice_locate',
57
+ description:
58
+ '用經緯度 (lat,lng) 反查所在的台灣縣市與行政區,並回傳該區實價頁連結。適合「我現在在這個座標,附近房價如何」。資料來源:housetw.com。',
59
+ inputSchema: {
60
+ type: 'object',
61
+ properties: {
62
+ lat: { type: 'number', description: '緯度,例如 25.034' },
63
+ lng: { type: 'number', description: '經度,例如 121.5645' },
64
+ },
65
+ required: ['lat', 'lng'],
66
+ },
67
+ run: (a) => realpriceLocate(a.lat, a.lng),
68
+ },
69
+ {
70
+ name: 'taiwan_realprice_area',
71
+ description:
72
+ '查某縣市 / 行政區的不動產成交行情統計:中位與平均單價(萬/坪)、成交筆數、平均屋齡、價格分位、熱門路段排行。資料來源:housetw.com(內政部實價登錄)。',
73
+ inputSchema: {
74
+ type: 'object',
75
+ properties: {
76
+ county: { type: 'string', description: '縣市,例如「臺北市」「新北市」(用「臺」非「台」)' },
77
+ district: { type: 'string', description: '行政區(可選),例如「信義區」' },
78
+ },
79
+ required: ['county'],
80
+ },
81
+ run: (a) => realpriceArea(a.county, a.district),
82
+ },
83
+ ];
84
+
85
+ const server = new Server(
86
+ { name: 'taiwan-data-mcp', version: '0.1.0' },
87
+ { capabilities: { tools: {} } }
88
+ );
89
+
90
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
91
+ tools: TOOLS.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })),
92
+ }));
93
+
94
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
95
+ const tool = TOOLS.find((t) => t.name === req.params.name);
96
+ if (!tool) return { content: [{ type: 'text', text: `未知工具:${req.params.name}` }], isError: true };
97
+ try {
98
+ const result = await tool.run(req.params.arguments || {});
99
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], isError: !!result?.error };
100
+ } catch (e) {
101
+ return { content: [{ type: 'text', text: `工具執行錯誤:${e?.message || e}` }], isError: true };
102
+ }
103
+ });
104
+
105
+ const transport = new StdioServerTransport();
106
+ await server.connect(transport);
107
+ console.error('taiwan-data-mcp running (stdio) — 6 tools: 防詐 / 公司登記 / 實價登錄');
@@ -0,0 +1,97 @@
1
+ // 台灣公開資料來源:薄客戶端,呼叫各站既有 JSON API,資料留在來源站。
2
+ // 每個工具回傳都夾帶 source 與 link(署名 + 導流)。
3
+
4
+ const UA = 'taiwan-data-mcp/0.1 (+https://github.com/kwlin/taiwan-data-mcp)';
5
+
6
+ export async function fetchJson(url, { timeout = 12000 } = {}) {
7
+ const ctrl = new AbortController();
8
+ const t = setTimeout(() => ctrl.abort(), timeout);
9
+ try {
10
+ const r = await fetch(url, { headers: { 'user-agent': UA, accept: 'application/json' }, signal: ctrl.signal });
11
+ const text = await r.text();
12
+ let body = null;
13
+ try { body = JSON.parse(text); } catch { /* non-JSON (e.g. SSR HTML fallback) */ }
14
+ return { ok: r.ok, status: r.status, body, raw: text };
15
+ } finally {
16
+ clearTimeout(t);
17
+ }
18
+ }
19
+
20
+ // ── 防詐 fraud.tw ───────────────────────────────────────────────
21
+ export function normalizeDomain(input) {
22
+ let s = String(input || '').trim().toLowerCase();
23
+ s = s.replace(/^https?:\/\//, '').replace(/^www\./, '');
24
+ s = s.split('/')[0].split('?')[0].split('#')[0].split(':')[0];
25
+ return s;
26
+ }
27
+
28
+ export async function scamCheck(input) {
29
+ const domain = normalizeDomain(input);
30
+ if (!domain || !domain.includes('.')) return { error: '請提供有效網址或網域,例如 example.com' };
31
+ const { ok, body } = await fetchJson(`https://fraud.tw/api/check?d=${encodeURIComponent(domain)}`);
32
+ if (!ok || !body) return { error: `查詢失敗(fraud.tw)`, domain };
33
+ return body; // 已自帶 source / detail
34
+ }
35
+
36
+ // ── 公司登記 inc.com.tw ─────────────────────────────────────────
37
+ export async function companySearch(name) {
38
+ const q = String(name || '').trim();
39
+ if (q.length < 2) return { error: '公司名稱至少 2 個字' };
40
+ const { ok, body } = await fetchJson(`https://inc.com.tw/api/suggest?q=${encodeURIComponent(q)}`);
41
+ if (!ok || !Array.isArray(body)) return { error: '查詢失敗(inc.com.tw)', query: q };
42
+ const companies = body
43
+ .filter((x) => x.t === 'co')
44
+ .map((x) => ({ unified_business_no: x.id, name: x.n, representative: x.r || null, alias: x.a || null,
45
+ profile_url: `https://inc.com.tw/c/${x.id}` }));
46
+ return { query: q, count: companies.length, companies,
47
+ source: '台灣公司登記網 inc.com.tw' };
48
+ }
49
+
50
+ export async function companyProfile(id) {
51
+ const tin = String(id || '').replace(/\D/g, '');
52
+ if (tin.length !== 8) return { error: '統一編號必須是 8 位數字' };
53
+ const { ok, status, body } = await fetchJson(`https://inc.com.tw/api/company/${tin}`);
54
+ if (status === 404) return { error: `查無此統一編號 ${tin}` };
55
+ if (!ok || !body) return { error: '查詢失敗(inc.com.tw)', unified_business_no: tin };
56
+ const { source: _drop, ...rest } = body;
57
+ return { ...rest, profile_url: `https://inc.com.tw/c/${tin}`,
58
+ source: '台灣公司登記網 inc.com.tw' };
59
+ }
60
+
61
+ // ── 實價登錄 housetw.com ───────────────────────────────────────
62
+ export async function realpriceSearch(q) {
63
+ const query = String(q || '').trim();
64
+ if (query.length < 2) return { error: '搜尋關鍵字至少 2 個字(地址 / 路段 / 行政區)' };
65
+ const { ok, body } = await fetchJson(`https://housetw.com/api/search.json?q=${encodeURIComponent(query)}`);
66
+ if (!ok || !body || !Array.isArray(body.items)) return { error: '查詢失敗(housetw.com)', query };
67
+ return {
68
+ query,
69
+ items: body.items.map((it) => ({ ...it, link: it.href ? `https://housetw.com${it.href}` : undefined })),
70
+ source: '內政部不動產交易實價登錄 · 實價雷達 housetw.com',
71
+ };
72
+ }
73
+
74
+ export async function realpriceLocate(lat, lng) {
75
+ const la = Number(lat), ln = Number(lng);
76
+ if (!isFinite(la) || !isFinite(ln)) return { error: '請提供有效的 lat / lng 經緯度' };
77
+ const { ok, body } = await fetchJson(`https://housetw.com/api/near.json?lat=${la}&lng=${ln}`);
78
+ if (!ok || !body || body.error) return { error: '此座標查無對應行政區(housetw.com)', lat: la, lng: ln };
79
+ return { lat: la, lng: ln, county: body.county, district: body.district,
80
+ radar_available: !!body.radar,
81
+ link: body.url ? `https://housetw.com${body.url}` : undefined,
82
+ source: '內政部不動產交易實價登錄 · 實價雷達 housetw.com' };
83
+ }
84
+
85
+ export async function realpriceArea(county, district) {
86
+ const c = String(county || '').trim();
87
+ if (!c) return { error: '請提供縣市 county(可加行政區 district)' };
88
+ const qs = `county=${encodeURIComponent(c)}` + (district ? `&district=${encodeURIComponent(String(district).trim())}` : '');
89
+ const { ok, body, status } = await fetchJson(`https://housetw.com/api/area.json?${qs}`);
90
+ // area.json 端點尚未部署時優雅降級為搜尋連結
91
+ if (status === 404 || !body) {
92
+ const fallback = await realpriceSearch(`${c}${district || ''}`);
93
+ return { degraded: true, note: 'area.json 端點尚未上線,改回傳搜尋結果連結', ...fallback };
94
+ }
95
+ if (!ok) return { error: '查詢失敗(housetw.com)', county: c };
96
+ return body;
97
+ }