taiwan-data-mcp 0.1.0 → 0.3.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/README.md +5 -1
- package/package.json +1 -1
- package/src/server.mjs +57 -3
- package/src/sources.mjs +85 -2
package/README.md
CHANGED
|
@@ -10,10 +10,14 @@
|
|
|
10
10
|
|------|------|----------|
|
|
11
11
|
| `taiwan_scam_check` | 查網址 / 網域是否被 165 通報詐騙 | [fraud.tw](https://fraud.tw)(內政部警政署 165) |
|
|
12
12
|
| `taiwan_company_search` | 用公司名搜尋,拿統一編號與負責人 | [inc.com.tw](https://inc.com.tw)(經濟部公司登記) |
|
|
13
|
-
| `taiwan_company_profile` |
|
|
13
|
+
| `taiwan_company_profile` | 用統編查公司完整登記資料(含董監事) | inc.com.tw |
|
|
14
|
+
| `taiwan_person_companies` | 用人名查他擔任負責人/董監事的公司 | inc.com.tw |
|
|
15
|
+
| `taiwan_company_risk` | 公司風險查核:拒絕往來/勞動/環保裁罰紅旗 | inc.com.tw |
|
|
14
16
|
| `taiwan_realprice_search` | 搜尋實價登錄的地址 / 路段 / 行政區 | [housetw.com](https://housetw.com)(內政部實價登錄) |
|
|
15
17
|
| `taiwan_realprice_locate` | 用經緯度反查行政區與行情頁 | housetw.com |
|
|
16
18
|
| `taiwan_realprice_area` | 查某縣市 / 行政區成交行情統計 | housetw.com |
|
|
19
|
+
| `taiwan_realprice_estimate` | 自動估價:單價區間與推估總價 | housetw.com |
|
|
20
|
+
| `taiwan_realprice_road` | 查某路段成交行情與逐年走勢 | housetw.com |
|
|
17
21
|
|
|
18
22
|
跨工具串接是重點:例如「查這家公司 → 看它登記地址那區的房價 → 查它官網是不是詐騙」,一次問答內 AI 自己串起來。
|
|
19
23
|
|
package/package.json
CHANGED
package/src/server.mjs
CHANGED
|
@@ -3,8 +3,8 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
5
|
import {
|
|
6
|
-
scamCheck, companySearch, companyProfile,
|
|
7
|
-
realpriceSearch, realpriceLocate, realpriceArea,
|
|
6
|
+
scamCheck, companySearch, companyProfile, personCompanies, companyRisk,
|
|
7
|
+
realpriceSearch, realpriceLocate, realpriceArea, realpriceEstimate, realpriceRoad,
|
|
8
8
|
} from './sources.mjs';
|
|
9
9
|
|
|
10
10
|
const TOOLS = [
|
|
@@ -41,6 +41,28 @@ const TOOLS = [
|
|
|
41
41
|
},
|
|
42
42
|
run: (a) => companyProfile(a.unified_business_no),
|
|
43
43
|
},
|
|
44
|
+
{
|
|
45
|
+
name: 'taiwan_person_companies',
|
|
46
|
+
description:
|
|
47
|
+
'用人名查他擔任「負責人/董監事」的台灣公司,回傳關聯公司數與範例公司(公司關係/查老闆人脈用)。以姓名比對,可能含同名同姓。資料來源:inc.com.tw。',
|
|
48
|
+
inputSchema: {
|
|
49
|
+
type: 'object',
|
|
50
|
+
properties: { name: { type: 'string', description: '人名,例如「郭台銘」' } },
|
|
51
|
+
required: ['name'],
|
|
52
|
+
},
|
|
53
|
+
run: (a) => personCompanies(a.name),
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'taiwan_company_risk',
|
|
57
|
+
description:
|
|
58
|
+
'公司風險查核(盡職調查紅旗):用 8 位統編查該公司有無政府採購拒絕往來、勞動法令裁罰、環保裁罰,回傳風險等級與紅旗清單。資料來源:inc.com.tw(聚合政府公開資料)。',
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: { unified_business_no: { type: 'string', description: '8 位統一編號,例如 22099131' } },
|
|
62
|
+
required: ['unified_business_no'],
|
|
63
|
+
},
|
|
64
|
+
run: (a) => companyRisk(a.unified_business_no),
|
|
65
|
+
},
|
|
44
66
|
{
|
|
45
67
|
name: 'taiwan_realprice_search',
|
|
46
68
|
description:
|
|
@@ -80,6 +102,38 @@ const TOOLS = [
|
|
|
80
102
|
},
|
|
81
103
|
run: (a) => realpriceArea(a.county, a.district),
|
|
82
104
|
},
|
|
105
|
+
{
|
|
106
|
+
name: 'taiwan_realprice_estimate',
|
|
107
|
+
description:
|
|
108
|
+
'自動估價:輸入縣市+行政區(可加建物型態/屋齡/坪數),回傳可比案例的單價區間(萬/坪)與推估總價。資料來源:housetw.com(內政部實價登錄)。',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
county: { type: 'string', description: '縣市,例如「臺北市」(用「臺」非「台」)' },
|
|
113
|
+
district: { type: 'string', description: '行政區,例如「信義區」' },
|
|
114
|
+
building_type: { type: 'string', description: '建物型態(可選),例如「住宅大樓」「公寓」「華廈」' },
|
|
115
|
+
house_age: { type: 'number', description: '屋齡(可選,年)' },
|
|
116
|
+
area_ping: { type: 'number', description: '坪數(可選),給了才會推估總價' },
|
|
117
|
+
},
|
|
118
|
+
required: ['county', 'district'],
|
|
119
|
+
},
|
|
120
|
+
run: (a) => realpriceEstimate(a.county, a.district, { type: a.building_type, age: a.house_age, ping: a.area_ping }),
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'taiwan_realprice_road',
|
|
124
|
+
description:
|
|
125
|
+
'查某路段的不動產成交行情:單價統計(萬/坪)、成交筆數、屋齡與逐年價格走勢。資料來源:housetw.com(內政部實價登錄)。',
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: 'object',
|
|
128
|
+
properties: {
|
|
129
|
+
county: { type: 'string', description: '縣市,例如「臺北市」' },
|
|
130
|
+
district: { type: 'string', description: '行政區,例如「信義區」' },
|
|
131
|
+
road: { type: 'string', description: '路段,例如「松高路」「忠孝東路四段」' },
|
|
132
|
+
},
|
|
133
|
+
required: ['county', 'district', 'road'],
|
|
134
|
+
},
|
|
135
|
+
run: (a) => realpriceRoad(a.county, a.district, a.road),
|
|
136
|
+
},
|
|
83
137
|
];
|
|
84
138
|
|
|
85
139
|
const server = new Server(
|
|
@@ -104,4 +158,4 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
104
158
|
|
|
105
159
|
const transport = new StdioServerTransport();
|
|
106
160
|
await server.connect(transport);
|
|
107
|
-
console.error('taiwan-data-mcp running (stdio) —
|
|
161
|
+
console.error('taiwan-data-mcp running (stdio) — 10 tools: 防詐 / 公司登記 / 實價登錄');
|
package/src/sources.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// 台灣公開資料來源:薄客戶端,呼叫各站既有 JSON API,資料留在來源站。
|
|
2
2
|
// 每個工具回傳都夾帶 source 與 link(署名 + 導流)。
|
|
3
3
|
|
|
4
|
-
const UA = 'taiwan-data-mcp
|
|
4
|
+
const UA = 'taiwan-data-mcp (+https://github.com/kewelin/taiwan-data-mcp)';
|
|
5
5
|
|
|
6
6
|
export async function fetchJson(url, { timeout = 12000 } = {}) {
|
|
7
7
|
const ctrl = new AbortController();
|
|
@@ -12,6 +12,9 @@ export async function fetchJson(url, { timeout = 12000 } = {}) {
|
|
|
12
12
|
let body = null;
|
|
13
13
|
try { body = JSON.parse(text); } catch { /* non-JSON (e.g. SSR HTML fallback) */ }
|
|
14
14
|
return { ok: r.ok, status: r.status, body, raw: text };
|
|
15
|
+
} catch (e) {
|
|
16
|
+
// 網路 / TLS / 逾時錯誤:回結構化失敗,讓工具優雅降級而非拋例外
|
|
17
|
+
return { ok: false, status: 0, body: null, raw: '', netError: e?.cause?.code || e?.name || String(e) };
|
|
15
18
|
} finally {
|
|
16
19
|
clearTimeout(t);
|
|
17
20
|
}
|
|
@@ -50,7 +53,7 @@ export async function companySearch(name) {
|
|
|
50
53
|
export async function companyProfile(id) {
|
|
51
54
|
const tin = String(id || '').replace(/\D/g, '');
|
|
52
55
|
if (tin.length !== 8) return { error: '統一編號必須是 8 位數字' };
|
|
53
|
-
const { ok, status, body } = await fetchJson(`https://inc.com.tw/api/company/${tin}
|
|
56
|
+
const { ok, status, body } = await fetchJson(`https://inc.com.tw/api/company/${tin}`, { timeout: 20000 });
|
|
54
57
|
if (status === 404) return { error: `查無此統一編號 ${tin}` };
|
|
55
58
|
if (!ok || !body) return { error: '查詢失敗(inc.com.tw)', unified_business_no: tin };
|
|
56
59
|
const { source: _drop, ...rest } = body;
|
|
@@ -58,6 +61,52 @@ export async function companyProfile(id) {
|
|
|
58
61
|
source: '台灣公司登記網 inc.com.tw' };
|
|
59
62
|
}
|
|
60
63
|
|
|
64
|
+
export async function personCompanies(name) {
|
|
65
|
+
const q = String(name || '').trim();
|
|
66
|
+
if (q.length < 2) return { error: '人名至少 2 個字' };
|
|
67
|
+
const { ok, body } = await fetchJson(`https://inc.com.tw/api/suggest?q=${encodeURIComponent(q)}`);
|
|
68
|
+
if (!ok || !Array.isArray(body)) return { error: '查詢失敗(inc.com.tw)', query: q };
|
|
69
|
+
const people = body
|
|
70
|
+
.filter((x) => x.t === 'p')
|
|
71
|
+
.map((x) => ({ person: x.n, company_count: x.c, example_companies: x.eg || [],
|
|
72
|
+
profile_url: `https://inc.com.tw/p/${encodeURIComponent(x.n)}` }));
|
|
73
|
+
if (!people.length) return { query: q, count: 0, note: '查無此人擔任負責人/董監事的公司(或人名不完整)', people: [],
|
|
74
|
+
source: '台灣公司登記網 inc.com.tw' };
|
|
75
|
+
return { query: q, count: people.length, people,
|
|
76
|
+
note: '以姓名比對,可能含同名同姓,僅供參考',
|
|
77
|
+
source: '台灣公司登記網 inc.com.tw' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function companyRisk(id) {
|
|
81
|
+
const tin = String(id || '').replace(/\D/g, '');
|
|
82
|
+
if (tin.length !== 8) return { error: '統一編號必須是 8 位數字' };
|
|
83
|
+
const { ok, status, body } = await fetchJson(`https://inc.com.tw/api/company/${tin}`, { timeout: 20000 });
|
|
84
|
+
if (status === 404) return { error: `查無此統一編號 ${tin}` };
|
|
85
|
+
if (!ok || !body) return { error: '查詢失敗(inc.com.tw)', unified_business_no: tin };
|
|
86
|
+
const f = body.flags || {};
|
|
87
|
+
const redFlags = [];
|
|
88
|
+
if (f.government_debarment) redFlags.push('政府採購拒絕往來');
|
|
89
|
+
if (f.labor_penalties) redFlags.push(`勞動法令裁罰 ${f.labor_penalties} 筆`);
|
|
90
|
+
if (f.environmental_penalties) redFlags.push(`環保裁罰 ${f.environmental_penalties} 筆`);
|
|
91
|
+
const level = f.government_debarment ? 'high' : redFlags.length ? 'medium' : 'low';
|
|
92
|
+
return {
|
|
93
|
+
unified_business_no: tin,
|
|
94
|
+
name: body.name,
|
|
95
|
+
status: body.status,
|
|
96
|
+
listing: body.listing || null,
|
|
97
|
+
risk_level: level,
|
|
98
|
+
red_flags: redFlags,
|
|
99
|
+
detail: {
|
|
100
|
+
government_debarment: !!f.government_debarment,
|
|
101
|
+
labor_penalties: f.labor_penalties || 0,
|
|
102
|
+
environmental_penalties: f.environmental_penalties || 0,
|
|
103
|
+
},
|
|
104
|
+
note: '裁罰以公司名稱比對,可能含同名同稱;僅供參考,以政府原始公告為準',
|
|
105
|
+
profile_url: `https://inc.com.tw/c/${tin}`,
|
|
106
|
+
source: '台灣公司登記網 inc.com.tw',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
61
110
|
// ── 實價登錄 housetw.com ───────────────────────────────────────
|
|
62
111
|
export async function realpriceSearch(q) {
|
|
63
112
|
const query = String(q || '').trim();
|
|
@@ -95,3 +144,37 @@ export async function realpriceArea(county, district) {
|
|
|
95
144
|
if (!ok) return { error: '查詢失敗(housetw.com)', county: c };
|
|
96
145
|
return body;
|
|
97
146
|
}
|
|
147
|
+
|
|
148
|
+
export async function realpriceEstimate(county, district, { type, age, ping } = {}) {
|
|
149
|
+
const c = String(county || '').trim(), d = String(district || '').trim();
|
|
150
|
+
if (!c || !d) return { error: '請提供縣市 county 與行政區 district' };
|
|
151
|
+
const qp = new URLSearchParams({ county: c, district: d });
|
|
152
|
+
if (type) qp.set('type', String(type).trim());
|
|
153
|
+
if (age != null && age !== '') qp.set('age', String(age));
|
|
154
|
+
if (ping != null && ping !== '') qp.set('ping', String(ping));
|
|
155
|
+
const { ok, body, status } = await fetchJson(`https://housetw.com/api/estimate.json?${qp}`);
|
|
156
|
+
if (status === 404 || !body) return { error: '估價端點暫不可用(housetw.com)', county: c, district: d };
|
|
157
|
+
if (!ok) return { error: '查詢失敗(housetw.com)', county: c, district: d };
|
|
158
|
+
return body;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function realpriceRoad(county, district, road) {
|
|
162
|
+
const c = String(county || '').trim(), d = String(district || '').trim(), r = String(road || '').trim();
|
|
163
|
+
if (!c || !d || !r) return { error: '請提供縣市 county、行政區 district、路段 road' };
|
|
164
|
+
const qp = new URLSearchParams({ county: c, district: d, road: r });
|
|
165
|
+
const { ok, body, status } = await fetchJson(`https://housetw.com/api/road.json?${qp}`);
|
|
166
|
+
if (status === 404 || !body) return { error: '路段端點暫不可用(housetw.com)', road: r };
|
|
167
|
+
if (!ok) return { error: '查詢失敗(housetw.com)', road: r };
|
|
168
|
+
return body;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function realpriceRent(county, district) {
|
|
172
|
+
const c = String(county || '').trim();
|
|
173
|
+
if (!c) return { error: '請提供縣市 county(可加行政區 district)' };
|
|
174
|
+
const qp = new URLSearchParams({ county: c });
|
|
175
|
+
if (district) qp.set('district', String(district).trim());
|
|
176
|
+
const { ok, body, status } = await fetchJson(`https://housetw.com/api/rent.json?${qp}`);
|
|
177
|
+
if (status === 404 || status === 302 || !body) return { error: '租金端點暫不可用(housetw.com,待部署)', county: c };
|
|
178
|
+
if (!ok) return { error: '查詢失敗(housetw.com)', county: c };
|
|
179
|
+
return body;
|
|
180
|
+
}
|