taiwan-data-mcp 0.1.0 → 0.2.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 CHANGED
@@ -10,10 +10,13 @@
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` | 用統編查公司完整登記資料 | inc.com.tw |
13
+ | `taiwan_company_profile` | 用統編查公司完整登記資料(含董監事) | inc.com.tw |
14
+ | `taiwan_person_companies` | 用人名查他擔任負責人/董監事的公司 | inc.com.tw |
14
15
  | `taiwan_realprice_search` | 搜尋實價登錄的地址 / 路段 / 行政區 | [housetw.com](https://housetw.com)(內政部實價登錄) |
15
16
  | `taiwan_realprice_locate` | 用經緯度反查行政區與行情頁 | housetw.com |
16
17
  | `taiwan_realprice_area` | 查某縣市 / 行政區成交行情統計 | housetw.com |
18
+ | `taiwan_realprice_estimate` | 自動估價:單價區間與推估總價 | housetw.com |
19
+ | `taiwan_realprice_road` | 查某路段成交行情與逐年走勢 | housetw.com |
17
20
 
18
21
  跨工具串接是重點:例如「查這家公司 → 看它登記地址那區的房價 → 查它官網是不是詐騙」,一次問答內 AI 自己串起來。
19
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taiwan-data-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server for Taiwan public data — 實價登錄行情、公司登記查核、165 防詐查詢。Lets AI assistants (Claude, Cursor, etc.) query real Taiwan data.",
5
5
  "type": "module",
6
6
  "bin": {
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,
7
+ realpriceSearch, realpriceLocate, realpriceArea, realpriceEstimate, realpriceRoad,
8
8
  } from './sources.mjs';
9
9
 
10
10
  const TOOLS = [
@@ -41,6 +41,17 @@ 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
+ },
44
55
  {
45
56
  name: 'taiwan_realprice_search',
46
57
  description:
@@ -80,6 +91,38 @@ const TOOLS = [
80
91
  },
81
92
  run: (a) => realpriceArea(a.county, a.district),
82
93
  },
94
+ {
95
+ name: 'taiwan_realprice_estimate',
96
+ description:
97
+ '自動估價:輸入縣市+行政區(可加建物型態/屋齡/坪數),回傳可比案例的單價區間(萬/坪)與推估總價。資料來源:housetw.com(內政部實價登錄)。',
98
+ inputSchema: {
99
+ type: 'object',
100
+ properties: {
101
+ county: { type: 'string', description: '縣市,例如「臺北市」(用「臺」非「台」)' },
102
+ district: { type: 'string', description: '行政區,例如「信義區」' },
103
+ building_type: { type: 'string', description: '建物型態(可選),例如「住宅大樓」「公寓」「華廈」' },
104
+ house_age: { type: 'number', description: '屋齡(可選,年)' },
105
+ area_ping: { type: 'number', description: '坪數(可選),給了才會推估總價' },
106
+ },
107
+ required: ['county', 'district'],
108
+ },
109
+ run: (a) => realpriceEstimate(a.county, a.district, { type: a.building_type, age: a.house_age, ping: a.area_ping }),
110
+ },
111
+ {
112
+ name: 'taiwan_realprice_road',
113
+ description:
114
+ '查某路段的不動產成交行情:單價統計(萬/坪)、成交筆數、屋齡與逐年價格走勢。資料來源:housetw.com(內政部實價登錄)。',
115
+ inputSchema: {
116
+ type: 'object',
117
+ properties: {
118
+ county: { type: 'string', description: '縣市,例如「臺北市」' },
119
+ district: { type: 'string', description: '行政區,例如「信義區」' },
120
+ road: { type: 'string', description: '路段,例如「松高路」「忠孝東路四段」' },
121
+ },
122
+ required: ['county', 'district', 'road'],
123
+ },
124
+ run: (a) => realpriceRoad(a.county, a.district, a.road),
125
+ },
83
126
  ];
84
127
 
85
128
  const server = new Server(
@@ -104,4 +147,4 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
104
147
 
105
148
  const transport = new StdioServerTransport();
106
149
  await server.connect(transport);
107
- console.error('taiwan-data-mcp running (stdio) — 6 tools: 防詐 / 公司登記 / 實價登錄');
150
+ console.error('taiwan-data-mcp running (stdio) — 9 tools: 防詐 / 公司登記 / 實價登錄');
package/src/sources.mjs CHANGED
@@ -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
  }
@@ -58,6 +61,22 @@ 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
+
61
80
  // ── 實價登錄 housetw.com ───────────────────────────────────────
62
81
  export async function realpriceSearch(q) {
63
82
  const query = String(q || '').trim();
@@ -95,3 +114,26 @@ export async function realpriceArea(county, district) {
95
114
  if (!ok) return { error: '查詢失敗(housetw.com)', county: c };
96
115
  return body;
97
116
  }
117
+
118
+ export async function realpriceEstimate(county, district, { type, age, ping } = {}) {
119
+ const c = String(county || '').trim(), d = String(district || '').trim();
120
+ if (!c || !d) return { error: '請提供縣市 county 與行政區 district' };
121
+ const qp = new URLSearchParams({ county: c, district: d });
122
+ if (type) qp.set('type', String(type).trim());
123
+ if (age != null && age !== '') qp.set('age', String(age));
124
+ if (ping != null && ping !== '') qp.set('ping', String(ping));
125
+ const { ok, body, status } = await fetchJson(`https://housetw.com/api/estimate.json?${qp}`);
126
+ if (status === 404 || !body) return { error: '估價端點暫不可用(housetw.com)', county: c, district: d };
127
+ if (!ok) return { error: '查詢失敗(housetw.com)', county: c, district: d };
128
+ return body;
129
+ }
130
+
131
+ export async function realpriceRoad(county, district, road) {
132
+ const c = String(county || '').trim(), d = String(district || '').trim(), r = String(road || '').trim();
133
+ if (!c || !d || !r) return { error: '請提供縣市 county、行政區 district、路段 road' };
134
+ const qp = new URLSearchParams({ county: c, district: d, road: r });
135
+ const { ok, body, status } = await fetchJson(`https://housetw.com/api/road.json?${qp}`);
136
+ if (status === 404 || !body) return { error: '路段端點暫不可用(housetw.com)', road: r };
137
+ if (!ok) return { error: '查詢失敗(housetw.com)', road: r };
138
+ return body;
139
+ }