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.
@@ -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
- return { seed, doc: docs[0] ?? null };
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 (revenue_min && !revenueMatchesMin(doc.revenue, revenue_min))
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}回)${usageFooter()}`,
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
- const stats = `\n\n---\nシード: ${seedTotal}社中${seeds.length}社取得 / API照会: ${apiCalls}回 / マッチ: ${prospects.length}社`;
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bizgate-mcp-server",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "description": "BizGate APIとClaudeを連携するMCPサーバー",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",