bizgate-mcp-server 0.3.8 → 0.3.9
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/dist/bizgate-cache.d.ts +35 -0
- package/dist/bizgate-cache.js +166 -0
- package/dist/index.js +78 -21
- package/package.json +1 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface CachedCompany {
|
|
2
|
+
corporate_number: string;
|
|
3
|
+
name: string;
|
|
4
|
+
revenue: string | null;
|
|
5
|
+
industry: string | null;
|
|
6
|
+
emp: string | null;
|
|
7
|
+
prefecture: string | null;
|
|
8
|
+
address: string | null;
|
|
9
|
+
tel: string | null;
|
|
10
|
+
hpurl: string | null;
|
|
11
|
+
pub: string | null;
|
|
12
|
+
shihon: string | null;
|
|
13
|
+
ceo: string | null;
|
|
14
|
+
}
|
|
15
|
+
/** BizGate API の結果を SQLite に永続キャッシュ(ローカル + R2共有) */
|
|
16
|
+
export declare class BizGateResultCache {
|
|
17
|
+
private db;
|
|
18
|
+
private sharedCacheUrl;
|
|
19
|
+
constructor(sharedCacheUrl?: string);
|
|
20
|
+
/** R2から共有キャッシュをダウンロードしてローカルにマージ */
|
|
21
|
+
syncSharedCache(): Promise<void>;
|
|
22
|
+
/** キャッシュから企業情報を取得 */
|
|
23
|
+
get(compno: string): CachedCompany | null;
|
|
24
|
+
/** 企業情報をキャッシュに保存 */
|
|
25
|
+
set(data: CachedCompany): void;
|
|
26
|
+
/** 複数の法人番号を一括チェック → キャッシュにある/ないを分離 */
|
|
27
|
+
partition(compnos: string[]): {
|
|
28
|
+
cached: CachedCompany[];
|
|
29
|
+
uncached: string[];
|
|
30
|
+
};
|
|
31
|
+
/** キャッシュ件数 */
|
|
32
|
+
count(): number;
|
|
33
|
+
/** 新規キャッシュをWorkerにアップロード(バックグラウンド) */
|
|
34
|
+
uploadToWorker(entries: CachedCompany[]): void;
|
|
35
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import Database from "better-sqlite3";
|
|
2
|
+
import { mkdirSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
const CACHE_DIR = join(homedir(), ".bizgate", "cache");
|
|
6
|
+
const DB_PATH = join(CACHE_DIR, "bizgate-results.db");
|
|
7
|
+
// SHARED_DB_PATH は JSONL 方式に移行したため不要
|
|
8
|
+
const SHARED_META_PATH = join(CACHE_DIR, "shared-meta.json");
|
|
9
|
+
const SHARED_TTL_DAYS = 1; // 共有キャッシュは毎日チェック
|
|
10
|
+
/** BizGate API の結果を SQLite に永続キャッシュ(ローカル + R2共有) */
|
|
11
|
+
export class BizGateResultCache {
|
|
12
|
+
db;
|
|
13
|
+
sharedCacheUrl;
|
|
14
|
+
constructor(sharedCacheUrl) {
|
|
15
|
+
this.sharedCacheUrl = sharedCacheUrl ?? "";
|
|
16
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
17
|
+
this.db = new Database(DB_PATH);
|
|
18
|
+
this.db.pragma("journal_mode = WAL");
|
|
19
|
+
this.db.exec(`
|
|
20
|
+
CREATE TABLE IF NOT EXISTS company_cache (
|
|
21
|
+
corporate_number TEXT PRIMARY KEY,
|
|
22
|
+
name TEXT,
|
|
23
|
+
revenue TEXT,
|
|
24
|
+
industry TEXT,
|
|
25
|
+
emp TEXT,
|
|
26
|
+
prefecture TEXT,
|
|
27
|
+
address TEXT,
|
|
28
|
+
tel TEXT,
|
|
29
|
+
hpurl TEXT,
|
|
30
|
+
pub TEXT,
|
|
31
|
+
shihon TEXT,
|
|
32
|
+
ceo TEXT,
|
|
33
|
+
cached_at TEXT NOT NULL
|
|
34
|
+
);
|
|
35
|
+
`);
|
|
36
|
+
}
|
|
37
|
+
/** R2から共有キャッシュをダウンロードしてローカルにマージ */
|
|
38
|
+
async syncSharedCache() {
|
|
39
|
+
if (!this.sharedCacheUrl)
|
|
40
|
+
return;
|
|
41
|
+
// TTLチェック
|
|
42
|
+
if (existsSync(SHARED_META_PATH)) {
|
|
43
|
+
try {
|
|
44
|
+
const meta = JSON.parse(readFileSync(SHARED_META_PATH, "utf-8"));
|
|
45
|
+
const lastSync = new Date(meta.lastSync);
|
|
46
|
+
const daysDiff = (Date.now() - lastSync.getTime()) / (1000 * 60 * 60 * 24);
|
|
47
|
+
if (daysDiff < SHARED_TTL_DAYS) {
|
|
48
|
+
console.error(`Shared cache is fresh (${Math.round(daysDiff * 24)}h ago), skipping sync`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch { /* proceed with sync */ }
|
|
53
|
+
}
|
|
54
|
+
console.error("Downloading shared BizGate cache from R2...");
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch(this.sharedCacheUrl);
|
|
57
|
+
if (!res.ok || !res.body) {
|
|
58
|
+
console.error(`Shared cache download failed: HTTP ${res.status}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Worker/R2 から JSONL をダウンロードしてローカルにマージ
|
|
62
|
+
const text = await res.text();
|
|
63
|
+
const entries = text
|
|
64
|
+
.split("\n")
|
|
65
|
+
.filter((l) => l.trim())
|
|
66
|
+
.map((l) => {
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(l);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
.filter((e) => e !== null);
|
|
75
|
+
if (entries.length > 0) {
|
|
76
|
+
const upsert = this.db.prepare(`
|
|
77
|
+
INSERT OR IGNORE INTO company_cache
|
|
78
|
+
(corporate_number, name, revenue, industry, emp, prefecture, address, tel, hpurl, pub, shihon, ceo, cached_at)
|
|
79
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
80
|
+
`);
|
|
81
|
+
const batchUpsert = this.db.transaction((items) => {
|
|
82
|
+
for (const r of items) {
|
|
83
|
+
upsert.run(r.corporate_number, r.name, r.revenue, r.industry, r.emp, r.prefecture, r.address, r.tel, r.hpurl, r.pub, r.shihon, r.ceo, r.cached_at);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
batchUpsert(entries);
|
|
87
|
+
console.error(`Shared cache merged: ${entries.length} companies`);
|
|
88
|
+
}
|
|
89
|
+
// メタ更新
|
|
90
|
+
writeFileSync(SHARED_META_PATH, JSON.stringify({ lastSync: new Date().toISOString() }), "utf-8");
|
|
91
|
+
}
|
|
92
|
+
catch (e) {
|
|
93
|
+
console.error(`Shared cache sync error: ${e instanceof Error ? e.message : e}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** キャッシュから企業情報を取得 */
|
|
97
|
+
get(compno) {
|
|
98
|
+
const row = this.db.prepare("SELECT * FROM company_cache WHERE corporate_number = ?").get(compno);
|
|
99
|
+
if (!row)
|
|
100
|
+
return null;
|
|
101
|
+
return {
|
|
102
|
+
corporate_number: row.corporate_number,
|
|
103
|
+
name: row.name,
|
|
104
|
+
revenue: row.revenue,
|
|
105
|
+
industry: row.industry,
|
|
106
|
+
emp: row.emp,
|
|
107
|
+
prefecture: row.prefecture,
|
|
108
|
+
address: row.address,
|
|
109
|
+
tel: row.tel,
|
|
110
|
+
hpurl: row.hpurl,
|
|
111
|
+
pub: row.pub,
|
|
112
|
+
shihon: row.shihon,
|
|
113
|
+
ceo: row.ceo,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** 企業情報をキャッシュに保存 */
|
|
117
|
+
set(data) {
|
|
118
|
+
this.db.prepare(`
|
|
119
|
+
INSERT OR REPLACE INTO company_cache
|
|
120
|
+
(corporate_number, name, revenue, industry, emp, prefecture, address, tel, hpurl, pub, shihon, ceo, cached_at)
|
|
121
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
122
|
+
`).run(data.corporate_number, data.name, data.revenue, data.industry, data.emp, data.prefecture, data.address, data.tel, data.hpurl, data.pub, data.shihon, data.ceo, new Date().toISOString());
|
|
123
|
+
}
|
|
124
|
+
/** 複数の法人番号を一括チェック → キャッシュにある/ないを分離 */
|
|
125
|
+
partition(compnos) {
|
|
126
|
+
const cached = [];
|
|
127
|
+
const uncached = [];
|
|
128
|
+
for (const compno of compnos) {
|
|
129
|
+
const hit = this.get(compno);
|
|
130
|
+
if (hit) {
|
|
131
|
+
cached.push(hit);
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
uncached.push(compno);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return { cached, uncached };
|
|
138
|
+
}
|
|
139
|
+
/** キャッシュ件数 */
|
|
140
|
+
count() {
|
|
141
|
+
const row = this.db.prepare("SELECT COUNT(*) as cnt FROM company_cache").get();
|
|
142
|
+
return row.cnt;
|
|
143
|
+
}
|
|
144
|
+
/** 新規キャッシュをWorkerにアップロード(バックグラウンド) */
|
|
145
|
+
uploadToWorker(entries) {
|
|
146
|
+
if (!this.sharedCacheUrl || entries.length === 0)
|
|
147
|
+
return;
|
|
148
|
+
// Worker URL を推測 (download URL → upload URL)
|
|
149
|
+
const workerUrl = process.env.CACHE_WORKER_URL ?? "https://bizgate-cache-worker.a-adachi.workers.dev";
|
|
150
|
+
if (!workerUrl)
|
|
151
|
+
return;
|
|
152
|
+
const payload = entries.map((e) => ({
|
|
153
|
+
...e,
|
|
154
|
+
cached_at: new Date().toISOString(),
|
|
155
|
+
}));
|
|
156
|
+
// 非同期で送信(失敗しても無視)
|
|
157
|
+
fetch(`${workerUrl}/upload`, {
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers: {
|
|
160
|
+
"Content-Type": "application/json",
|
|
161
|
+
"Authorization": `Bearer ${process.env.CACHE_WORKER_TOKEN ?? ""}`,
|
|
162
|
+
},
|
|
163
|
+
body: JSON.stringify(payload),
|
|
164
|
+
}).catch(() => { });
|
|
165
|
+
}
|
|
166
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { BizGateClient, first as f } from "./bizgate-client.js";
|
|
|
12
12
|
import { BizGateApiError, DEPARTMENT_CATEGORIES } from "./types.js";
|
|
13
13
|
import { UsageTracker } from "./usage-tracker.js";
|
|
14
14
|
import { SeedCache } from "./seed-cache.js";
|
|
15
|
+
import { BizGateResultCache } from "./bizgate-cache.js";
|
|
15
16
|
import { JpxCache } from "./jpx-cache.js";
|
|
16
17
|
import { homedir } from "node:os";
|
|
17
18
|
import { join, dirname } from "node:path";
|
|
@@ -40,6 +41,7 @@ if (authMode === "ip" && !process.env.BIZGATE_APP) {
|
|
|
40
41
|
}
|
|
41
42
|
const dailyLimit = Number(process.env.BIZGATE_DAILY_LIMIT ?? "200");
|
|
42
43
|
const seedCsvUrl = process.env.SEED_CSV_URL ?? "https://pub-3952dc5d8b37475196bab871e4e93bc1.r2.dev/latest/seed-list.csv.gz";
|
|
44
|
+
const sharedCacheUrl = process.env.SHARED_CACHE_URL ?? "https://bizgate-cache-worker.a-adachi.workers.dev/download";
|
|
43
45
|
const usageFile = process.env.BIZGATE_USAGE_FILE ??
|
|
44
46
|
join(homedir(), ".bizgate-mcp-usage.json");
|
|
45
47
|
const baseUrl = process.env.BIZGATE_BASE_URL ??
|
|
@@ -61,6 +63,7 @@ const config = {
|
|
|
61
63
|
const usageTracker = new UsageTracker(usageFile, dailyLimit);
|
|
62
64
|
const client = new BizGateClient(config, usageTracker);
|
|
63
65
|
const seedCache = new SeedCache(seedCsvUrl);
|
|
66
|
+
const resultCache = new BizGateResultCache(sharedCacheUrl);
|
|
64
67
|
const jpxCache = new JpxCache(process.env.JPX_DATA_URL);
|
|
65
68
|
// ---------- MCPサーバー ----------
|
|
66
69
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -490,18 +493,81 @@ server.tool("bizgate__prospect_list", "シードリスト(国税庁572万法
|
|
|
490
493
|
content: [{ type: "text", text: `条件に合う企業がシードリストに見つかりませんでした。${usageFooter()}` }],
|
|
491
494
|
};
|
|
492
495
|
}
|
|
493
|
-
// ---- Step 2: BizGate企業検索 → 売上/業種/従業員フィルタ ----
|
|
496
|
+
// ---- Step 2: キャッシュ確認 → BizGate企業検索 → 売上/業種/従業員フィルタ ----
|
|
494
497
|
const verified = [];
|
|
498
|
+
const newCacheEntries = [];
|
|
495
499
|
let apiCalls = 0;
|
|
500
|
+
let cacheHits = 0;
|
|
496
501
|
const batchSize = 5;
|
|
497
|
-
const targetVerified = Math.ceil(count * 1.5);
|
|
502
|
+
const targetVerified = Math.ceil(count * 1.5);
|
|
503
|
+
// フィルタ関数(キャッシュ・API結果の両方で使う)
|
|
504
|
+
function matchesFilters(revenue, industryVal, empVal) {
|
|
505
|
+
if (revenue_min && !revenueMatchesMin(revenue ?? undefined, revenue_min))
|
|
506
|
+
return false;
|
|
507
|
+
if (industry) {
|
|
508
|
+
const ind = industryVal ?? "";
|
|
509
|
+
if (!industry.split(",").some((k) => ind.includes(k.trim())))
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
if (employee_500plus && empVal) {
|
|
513
|
+
const empCat = Number(empVal.match(/^(\d+)\./)?.[1] ?? 0);
|
|
514
|
+
if (empCat < 6)
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
return true;
|
|
518
|
+
}
|
|
498
519
|
for (let i = 0; i < seeds.length && verified.length < targetVerified && apiCalls < MAX_API_CALLS_HARD_LIMIT; i += batchSize) {
|
|
499
520
|
const batch = seeds.slice(i, i + batchSize);
|
|
500
521
|
const results = await Promise.all(batch.map(async (seed) => {
|
|
522
|
+
// キャッシュ確認
|
|
523
|
+
const cached = resultCache.get(seed.corporate_number);
|
|
524
|
+
if (cached) {
|
|
525
|
+
cacheHits++;
|
|
526
|
+
if (!matchesFilters(cached.revenue, cached.industry, cached.emp)) {
|
|
527
|
+
return { seed, doc: null };
|
|
528
|
+
}
|
|
529
|
+
// キャッシュヒット → BizGateDocとして組み立て(API消費なし)
|
|
530
|
+
const fakeDoc = {
|
|
531
|
+
compno: cached.corporate_number,
|
|
532
|
+
shogo: [cached.name ?? ""],
|
|
533
|
+
revenue: cached.revenue ?? undefined,
|
|
534
|
+
gyoshu_facet: cached.industry ? [cached.industry] : undefined,
|
|
535
|
+
emp: cached.emp ?? undefined,
|
|
536
|
+
add: cached.address ? [cached.address] : undefined,
|
|
537
|
+
pref: cached.prefecture ? [cached.prefecture] : undefined,
|
|
538
|
+
tel: cached.tel ? [cached.tel] : undefined,
|
|
539
|
+
hpurl: cached.hpurl ? [cached.hpurl] : undefined,
|
|
540
|
+
pub: cached.pub ?? undefined,
|
|
541
|
+
shihon: cached.shihon ?? undefined,
|
|
542
|
+
ceo: cached.ceo ? [cached.ceo] : undefined,
|
|
543
|
+
};
|
|
544
|
+
return { seed, doc: fakeDoc };
|
|
545
|
+
}
|
|
546
|
+
// キャッシュなし → API呼び出し
|
|
501
547
|
try {
|
|
502
548
|
const { docs } = await client.searchCompany({ compno: seed.corporate_number });
|
|
503
549
|
apiCalls++;
|
|
504
|
-
|
|
550
|
+
const doc = docs[0] ?? null;
|
|
551
|
+
// 結果をキャッシュに保存(マッチしなくてもキャッシュ → 次回API不要)
|
|
552
|
+
if (doc) {
|
|
553
|
+
const cached = {
|
|
554
|
+
corporate_number: seed.corporate_number,
|
|
555
|
+
name: f(doc.shogo),
|
|
556
|
+
revenue: doc.revenue ?? null,
|
|
557
|
+
industry: f(doc.gyoshu_facet) || null,
|
|
558
|
+
emp: doc.emp ?? null,
|
|
559
|
+
prefecture: f(doc.pref) || null,
|
|
560
|
+
address: f(doc.add) || null,
|
|
561
|
+
tel: f(doc.tel) || null,
|
|
562
|
+
hpurl: f(doc.hpurl) || null,
|
|
563
|
+
pub: doc.pub ?? null,
|
|
564
|
+
shihon: doc.shihon ?? null,
|
|
565
|
+
ceo: f(doc.ceo) || null,
|
|
566
|
+
};
|
|
567
|
+
resultCache.set(cached);
|
|
568
|
+
newCacheEntries.push(cached);
|
|
569
|
+
}
|
|
570
|
+
return { seed, doc };
|
|
505
571
|
}
|
|
506
572
|
catch {
|
|
507
573
|
apiCalls++;
|
|
@@ -511,22 +577,8 @@ server.tool("bizgate__prospect_list", "シードリスト(国税庁572万法
|
|
|
511
577
|
for (const { seed, doc } of results) {
|
|
512
578
|
if (!doc)
|
|
513
579
|
continue;
|
|
514
|
-
if (
|
|
580
|
+
if (!matchesFilters(doc.revenue, f(doc.gyoshu_facet), doc.emp))
|
|
515
581
|
continue;
|
|
516
|
-
if (industry) {
|
|
517
|
-
const docIndustry = f(doc.gyoshu_facet);
|
|
518
|
-
if (!industry.split(",").some((k) => docIndustry.includes(k.trim())))
|
|
519
|
-
continue;
|
|
520
|
-
}
|
|
521
|
-
if (employee_500plus && doc.emp) {
|
|
522
|
-
const empMatch = doc.emp.match(/(\d+)/);
|
|
523
|
-
if (empMatch) {
|
|
524
|
-
// emp例: "6. 500-1000人未満" → カテゴリ6以上
|
|
525
|
-
const empCat = Number(doc.emp.match(/^(\d+)\./)?.[1] ?? 0);
|
|
526
|
-
if (empCat < 6)
|
|
527
|
-
continue; // カテゴリ6 = 500-1000人
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
582
|
verified.push({ doc, seed });
|
|
531
583
|
}
|
|
532
584
|
}
|
|
@@ -534,7 +586,7 @@ server.tool("bizgate__prospect_list", "シードリスト(国税庁572万法
|
|
|
534
586
|
return {
|
|
535
587
|
content: [{
|
|
536
588
|
type: "text",
|
|
537
|
-
text: `条件に合う企業が見つかりませんでした。\n(シード候補: ${seeds.length}社 / API照会: ${apiCalls}
|
|
589
|
+
text: `条件に合う企業が見つかりませんでした。\n(シード候補: ${seeds.length}社 / API照会: ${apiCalls}回 / キャッシュヒット: ${cacheHits}件)${usageFooter()}`,
|
|
538
590
|
}],
|
|
539
591
|
};
|
|
540
592
|
}
|
|
@@ -619,7 +671,11 @@ server.tool("bizgate__prospect_list", "シードリスト(国税庁572万法
|
|
|
619
671
|
return `| ${i + 1} | ${p.name} | ${p.department} | ${keymanStr} | ${p.phone} | ${p.approach_note} |`;
|
|
620
672
|
})
|
|
621
673
|
.join("\n");
|
|
622
|
-
|
|
674
|
+
// 新規キャッシュをWorkerに非同期アップロード
|
|
675
|
+
if (newCacheEntries.length > 0) {
|
|
676
|
+
resultCache.uploadToWorker(newCacheEntries);
|
|
677
|
+
}
|
|
678
|
+
const stats = `\n\n---\nシード: ${seedTotal}社中${seeds.length}社取得 / API照会: ${apiCalls}回 / キャッシュヒット: ${cacheHits}件 / 新規キャッシュ: ${newCacheEntries.length}件 / マッチ: ${prospects.length}社 / キャッシュ総数: ${resultCache.count()}件`;
|
|
623
679
|
return {
|
|
624
680
|
content: [{ type: "text", text: header + table + stats + usageFooter() }],
|
|
625
681
|
};
|
|
@@ -779,8 +835,9 @@ server.tool("bizgate__enterprise_search", "上場企業(JPX約3,800社)か
|
|
|
779
835
|
});
|
|
780
836
|
// ---------- 起動 ----------
|
|
781
837
|
async function main() {
|
|
782
|
-
//
|
|
838
|
+
// シードキャッシュ + 共有BizGateキャッシュを非同期で初期化
|
|
783
839
|
seedCache.init().catch((e) => console.error("Seed cache init error:", e));
|
|
840
|
+
resultCache.syncSharedCache().catch((e) => console.error("Shared cache sync error:", e));
|
|
784
841
|
jpxCache.init().catch((e) => console.error("JPX cache init error:", e));
|
|
785
842
|
const transport = new StdioServerTransport();
|
|
786
843
|
await server.connect(transport);
|