bizgate-mcp-server 0.3.9 → 0.3.10

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/index.js CHANGED
@@ -39,7 +39,7 @@ if (authMode === "ip" && !process.env.BIZGATE_APP) {
39
39
  console.error("Error: BIZGATE_APP is required when BIZGATE_AUTH_MODE=ip");
40
40
  process.exit(1);
41
41
  }
42
- const dailyLimit = Number(process.env.BIZGATE_DAILY_LIMIT ?? "200");
42
+ const dailyLimit = Number(process.env.BIZGATE_DAILY_LIMIT ?? "1000");
43
43
  const seedCsvUrl = process.env.SEED_CSV_URL ?? "https://pub-3952dc5d8b37475196bab871e4e93bc1.r2.dev/latest/seed-list.csv.gz";
44
44
  const sharedCacheUrl = process.env.SHARED_CACHE_URL ?? "https://bizgate-cache-worker.a-adachi.workers.dev/download";
45
45
  const usageFile = process.env.BIZGATE_USAGE_FILE ??
@@ -430,20 +430,27 @@ server.tool("bizgate__usage_status", "本日のBizGate API残り利用回数を
430
430
  });
431
431
  // ---------- Tool 7: プロスペクトリスト(ローカルSQLite) ----------
432
432
  const MAX_API_CALLS_HARD_LIMIT = 150;
433
+ /** 全角数字を半角に変換 */
434
+ function zenToHan(s) {
435
+ return s.replace(/[0-9]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 0xFEE0));
436
+ }
433
437
  /** 売上カテゴリ文字列から数値を抽出して最低ラインと比較 */
434
438
  function revenueMatchesMin(revenue, minLabel) {
435
439
  if (!revenue)
436
440
  return false;
437
- // revenue例: "7. 20億-100億未満", minLabel例: "50億以上"
441
+ // revenue例: "5.500億以上" (全角数字+全角ピリオド)
442
+ // カテゴリ: 1=5億未満, 2=5-20億, 3=20-100億, 4=100-500億, 5=500億以上
438
443
  const thresholds = {
439
- "50億以上": 7, // カテゴリ7(20億-100億) 以上
440
- "300億以上": 8, // カテゴリ8(100億以上)
441
- "1000億以上": 8, // カテゴリ8(100億以上)
444
+ "50億以上": 3, // カテゴリ3(20億-100億) 以上 — 50億含む可能性
445
+ "300億以上": 4, // カテゴリ4(100億-500億) 以上
446
+ "1000億以上": 5, // カテゴリ5(500億以上)
442
447
  };
443
448
  const minCat = thresholds[minLabel];
444
449
  if (!minCat)
445
- return true; // 不明なラベルはフィルタしない
446
- const match = revenue.match(/^(\d+)\./);
450
+ return true;
451
+ // 全角数字・全角ピリオドを半角に正規化してからマッチ
452
+ const normalized = zenToHan(revenue).replace(/./g, ".");
453
+ const match = normalized.match(/^(\d+)\./);
447
454
  if (!match)
448
455
  return false;
449
456
  return Number(match[1]) >= minCat;
@@ -465,10 +472,10 @@ server.tool("bizgate__prospect_list", "シードリスト(国税庁572万法
465
472
  city: z.string().optional().describe("市区町村名(例: 千代田区)"),
466
473
  industry: z.string().optional().describe("BizGate業種分類キーワード(例: 製造業, サービス, IT)"),
467
474
  revenue_min: z.string().optional().describe("最低売上(50億以上 / 300億以上 / 1000億以上)"),
468
- employee_500plus: z.boolean().optional().describe("従業員500人以上のみ"),
475
+ emp_min: z.string().optional().describe("最低従業員数(100人以上 / 300人以上 / 1000人以上)"),
469
476
  proposal_axis: z.union([z.string(), z.array(z.string())]).optional().describe("提案軸(DOMO / 営業DX/CRM)。部署検索のキーワードに変換される"),
470
477
  count: z.number().optional().describe("取得したい企業数(デフォルト: 10、最大: 30)"),
471
- }, async ({ pref, city, industry, revenue_min, employee_500plus, proposal_axis, count: rawCount }) => {
478
+ }, async ({ pref, city, industry, revenue_min, emp_min, proposal_axis, count: rawCount }) => {
472
479
  if (!seedCache.isReady()) {
473
480
  return {
474
481
  content: [{ type: "text", text: "エラー: シードリストが利用できません。SEED_CSV_URL を設定し、Claude Code を再起動してください。" }],
@@ -509,10 +516,19 @@ server.tool("bizgate__prospect_list", "シードリスト(国税庁572万法
509
516
  if (!industry.split(",").some((k) => ind.includes(k.trim())))
510
517
  return false;
511
518
  }
512
- if (employee_500plus && empVal) {
513
- const empCat = Number(empVal.match(/^(\d+)\./)?.[1] ?? 0);
514
- if (empCat < 6)
515
- return false;
519
+ if (emp_min && empVal) {
520
+ const empThresholds = {
521
+ "100人以上": 4, // カテゴリ4(100-300人) 以上
522
+ "300人以上": 5, // カテゴリ5(300-1000人) 以上
523
+ "1000人以上": 6, // カテゴリ6(1000人以上)
524
+ };
525
+ const minEmpCat = empThresholds[emp_min];
526
+ if (minEmpCat) {
527
+ const empNorm = zenToHan(empVal).replace(/./g, ".");
528
+ const empCat = Number(empNorm.match(/^(\d+)\./)?.[1] ?? 0);
529
+ if (empCat < minEmpCat)
530
+ return false;
531
+ }
516
532
  }
517
533
  return true;
518
534
  }
@@ -740,7 +756,14 @@ server.tool("bizgate__enterprise_search", "上場企業(JPX約3,800社)か
740
756
  : { shogo: listed.name };
741
757
  const { docs } = await client.searchCompany(searchParams);
742
758
  apiCalls++;
743
- return { listed, doc: docs[0] ?? null };
759
+ let doc = docs[0] ?? null;
760
+ // shogo検索ではrevenueが返らないため、compnoで再検索
761
+ if (doc && !doc.revenue && !listed.corporate_number && doc.compno) {
762
+ const { docs: docs2 } = await client.searchCompany({ compno: doc.compno });
763
+ apiCalls++;
764
+ doc = docs2[0] ?? doc;
765
+ }
766
+ return { listed, doc };
744
767
  }
745
768
  catch {
746
769
  apiCalls++;
@@ -833,6 +856,172 @@ server.tool("bizgate__enterprise_search", "上場企業(JPX約3,800社)か
833
856
  content: [{ type: "text", text: header + filterInfo + table + stats + usageFooter() }],
834
857
  };
835
858
  });
859
+ // ---------- Tool 9: prospect_suggest(ローカルDB候補+Claude判断 → API節約) ----------
860
+ server.tool("bizgate__prospect_suggest", "BizGate APIを使わずにローカルDB(上場企業JPX約3,800社 or シード約570万社)から候補企業リストを返す。sourceを省略すると利用可能な検索条件のガイドを返す(条件を知らないユーザー向け)。Claudeがリストから提案軸に合う企業を判断し、選んだ企業だけをbizgate__department_searchで照会する2段階ワークフロー用。API消費: 0回。", {
861
+ source: z.enum(["jpx", "seed"]).optional().describe("検索ソース。jpx=上場企業(市場・業種フィルタ可)、seed=全企業(都道府県・市区町村フィルタ可)。省略時は検索条件ガイドを表示"),
862
+ pref: z.string().optional().describe("都道府県(例: 東京都)。sourceがseedの場合のみ有効"),
863
+ city: z.string().optional().describe("市区町村(例: 渋谷区)。sourceがseedの場合のみ有効"),
864
+ keyword: z.string().optional().describe("企業名キーワード(部分一致)"),
865
+ market: z.string().optional().describe("市場区分(プライム / スタンダード / グロース)。sourceがjpxの場合のみ有効"),
866
+ industry: z.string().optional().describe("業種キーワード(例: 情報・通信業, 電気機器)。sourceがjpxの場合のみ有効"),
867
+ count: z.number().optional().describe("取得件数(デフォルト: 50、最大: 200)"),
868
+ shuffle: z.boolean().optional().describe("ランダム順にするか(デフォルト: false)"),
869
+ }, async ({ source, pref, city, keyword, market, industry, count: rawCount, shuffle }) => {
870
+ // ---- ガイドモード: sourceが省略された場合 ----
871
+ if (!source) {
872
+ const lines = [
873
+ "## 🔍 プロスペクト検索 — 条件ガイド",
874
+ "",
875
+ "このツールはローカルDBから候補企業を抽出します(**API消費 0回**)。",
876
+ "以下の条件を組み合わせて検索できます。",
877
+ "",
878
+ "---",
879
+ "",
880
+ "### データソース(`source` — 必須)",
881
+ "",
882
+ "| source | 対象 | 件数 | 使えるフィルタ |",
883
+ "|--------|------|------|--------------|",
884
+ "| `jpx` | 上場企業 | 約3,800社 | 市場区分, 業種, 企業名 |",
885
+ "| `seed` | 全法人(国税庁) | 約572万社 | 都道府県, 市区町村, 企業名 |",
886
+ "",
887
+ ];
888
+ // JPX: 市場別
889
+ if (jpxCache.isReady()) {
890
+ const markets = jpxCache.listMarkets();
891
+ lines.push("### 市場区分(`market` — jpxのみ)");
892
+ lines.push("");
893
+ lines.push("| 市場 | 企業数 |");
894
+ lines.push("|------|--------|");
895
+ for (const m of markets) {
896
+ lines.push(`| ${m.market} | ${m.count.toLocaleString()}社 |`);
897
+ }
898
+ // JPX: 業種
899
+ const industries = jpxCache.listIndustries();
900
+ lines.push("");
901
+ lines.push("### 業種(`industry` — jpxのみ、部分一致)");
902
+ lines.push("");
903
+ lines.push("| 業種 | 企業数 |");
904
+ lines.push("|------|--------|");
905
+ for (const ind of industries) {
906
+ lines.push(`| ${ind.industry} | ${ind.count.toLocaleString()}社 |`);
907
+ }
908
+ }
909
+ // Seed: 都道府県
910
+ if (seedCache.isReady()) {
911
+ const prefs = seedCache.listPrefectures();
912
+ lines.push("");
913
+ lines.push("### 都道府県(`pref` — seedのみ、完全一致)");
914
+ lines.push("");
915
+ lines.push("| 都道府県 | 法人数 |");
916
+ lines.push("|---------|--------|");
917
+ for (const p of prefs) {
918
+ lines.push(`| ${p.prefecture} | ${p.count.toLocaleString()}社 |`);
919
+ }
920
+ lines.push("");
921
+ lines.push("### 市区町村(`city` — seedのみ、完全一致)");
922
+ lines.push("都道府県と組み合わせて使用。例: `pref: \"東京都\", city: \"渋谷区\"`");
923
+ }
924
+ // 共通オプション
925
+ lines.push("");
926
+ lines.push("### 共通オプション");
927
+ lines.push("");
928
+ lines.push("| パラメータ | 説明 |");
929
+ lines.push("|-----------|------|");
930
+ lines.push("| `keyword` | 企業名キーワード(部分一致) |");
931
+ lines.push("| `count` | 取得件数(デフォルト50、最大200) |");
932
+ lines.push("| `shuffle` | ランダム順にするか(デフォルトfalse) |");
933
+ // BizGate API フィルタ(prospect_list / enterprise_search で使用)
934
+ lines.push("");
935
+ lines.push("---");
936
+ lines.push("");
937
+ lines.push("### 参考: BizGate APIフィルタ(`prospect_list` / `enterprise_search` で使用)");
938
+ lines.push("");
939
+ lines.push("以下はAPI消費ありのツールで使える追加フィルタです。");
940
+ lines.push("");
941
+ lines.push("**売上規模(`revenue_min`)**");
942
+ lines.push("| ラベル | BizGateカテゴリ |");
943
+ lines.push("|--------|---------------|");
944
+ lines.push("| `50億以上` | カテゴリ3~(20億-100億以上) |");
945
+ lines.push("| `300億以上` | カテゴリ4~(100億-500億以上) |");
946
+ lines.push("| `1000億以上` | カテゴリ5(500億以上) |");
947
+ lines.push("");
948
+ lines.push("**従業員数(`emp_min`)**");
949
+ lines.push("| ラベル | BizGateカテゴリ |");
950
+ lines.push("|--------|---------------|");
951
+ lines.push("| `100人以上` | カテゴリ4~(100-300人以上) |");
952
+ lines.push("| `300人以上` | カテゴリ5~(300-1000人以上) |");
953
+ lines.push("| `1000人以上` | カテゴリ6(1000人以上) |");
954
+ lines.push("");
955
+ lines.push("**提案軸(`proposal_axis`)**");
956
+ lines.push("| 軸 | 自動変換される部署キーワード |");
957
+ lines.push("|-----|---------------------------|");
958
+ lines.push("| `DOMO` | 経営企画, DX, システム, 情報, データ, 分析 |");
959
+ lines.push("| `営業DX/CRM` | 営業, マーケティング, 販売, 顧客, CRM, 販促 |");
960
+ return {
961
+ content: [{ type: "text", text: lines.join("\n") }],
962
+ };
963
+ }
964
+ // ---- JPXソース ----
965
+ const count = Math.min(rawCount ?? 50, 200);
966
+ if (source === "jpx") {
967
+ if (!jpxCache.isReady()) {
968
+ return {
969
+ content: [{ type: "text", text: "エラー: 上場企業データベースが利用できません。サーバーを再起動してください。" }],
970
+ };
971
+ }
972
+ const { total, results } = jpxCache.search({
973
+ market,
974
+ industry,
975
+ nameKeyword: keyword,
976
+ limit: count,
977
+ });
978
+ if (results.length === 0) {
979
+ return {
980
+ content: [{ type: "text", text: `条件に合う上場企業が見つかりませんでした。(検索条件: 市場=${market ?? "全て"}, 業種=${industry ?? "全て"}, キーワード=${keyword ?? "なし"})` }],
981
+ };
982
+ }
983
+ const table = "| # | 証券コード | 会社名 | 市場 | 業種 | 法人番号 |\n" +
984
+ "|---|-----------|--------|------|------|----------|\n" +
985
+ results
986
+ .map((r, i) => `| ${i + 1} | ${r.securities_code} | ${r.name} | ${r.market} | ${r.industry_33} | ${r.corporate_number ?? "-"} |`)
987
+ .join("\n");
988
+ return {
989
+ content: [{
990
+ type: "text",
991
+ text: `## 上場企業候補(${results.length}社 / 全${total}社)\n\n${table}\n\n---\nAPI消費: 0回\n\n> この中から提案軸に合う企業をClaudeが選び、選んだ企業のみ bizgate__department_search で部署・電話番号を取得してください。法人番号(compno)を使うと精度が高くなります。`,
992
+ }],
993
+ };
994
+ }
995
+ // ---- シードソース ----
996
+ if (!seedCache.isReady()) {
997
+ return {
998
+ content: [{ type: "text", text: "エラー: シードデータベースが利用できません。サーバーを再起動してください。" }],
999
+ };
1000
+ }
1001
+ const { total, results } = seedCache.search({
1002
+ pref,
1003
+ city,
1004
+ keyword,
1005
+ limit: count,
1006
+ shuffle: shuffle ?? false,
1007
+ });
1008
+ if (results.length === 0) {
1009
+ return {
1010
+ content: [{ type: "text", text: `条件に合う企業が見つかりませんでした。(検索条件: 都道府県=${pref ?? "全て"}, 市区町村=${city ?? "全て"}, キーワード=${keyword ?? "なし"})` }],
1011
+ };
1012
+ }
1013
+ const table = "| # | 会社名 | 都道府県 | 市区町村 | 法人番号 |\n" +
1014
+ "|---|--------|---------|---------|----------|\n" +
1015
+ results
1016
+ .map((r, i) => `| ${i + 1} | ${r.name} | ${r.prefecture} | ${r.city} | ${r.corporate_number} |`)
1017
+ .join("\n");
1018
+ return {
1019
+ content: [{
1020
+ type: "text",
1021
+ text: `## シード企業候補(${results.length}社 / 全${total}社)\n\n${table}\n\n---\nAPI消費: 0回\n\n> この中から提案軸に合う企業をClaudeが選び、選んだ企業のみ bizgate__department_search で部署・電話番号を取得してください。法人番号(compno)を使うと精度が高くなります。`,
1022
+ }],
1023
+ };
1024
+ });
836
1025
  // ---------- 起動 ----------
837
1026
  async function main() {
838
1027
  // シードキャッシュ + 共有BizGateキャッシュを非同期で初期化
@@ -2,7 +2,9 @@
2
2
  import { mkdirSync, writeFileSync, existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { homedir } from "node:os";
5
- const SKILL_DIR = join(homedir(), ".claude", "skills", "prospect-match");
5
+ const SKILLS_BASE = join(homedir(), ".claude", "skills");
6
+ const SKILL_DIR = join(SKILLS_BASE, "prospect-match");
7
+ const SKILL_FIND_DIR = join(SKILLS_BASE, "prospect-find");
6
8
  const SKILL_MD = `---
7
9
  name: prospect-match
8
10
  description: |
@@ -90,9 +92,120 @@ bKwd: "戦略,成長,事業創造,イノベーション,企画,開発,推進,DX,
90
92
 
91
93
  ---
92
94
 
95
+ $ARGUMENTS
96
+ `;
97
+ const SKILL_FIND_MD = `---
98
+ name: prospect-find
99
+ description: |
100
+ 条件を対話的に選択し、ローカルDBから候補企業を抽出 → Claude判断 → 部署検索の低API消費ワークフローを実行する。
101
+ "企業を探して", "プロスペクトを探して", "prospect find", "リスト作って",
102
+ "DOMOに合う企業", "営業先を探して", "ターゲット企業", "候補企業",
103
+ "기업을 찾아줘", "리스트 만들어줘", "영업 대상 찾아줘"
104
+ user-invocable: true
105
+ argument-hint: "[提案サービス名 or 自由条件]"
106
+ allowed-tools: mcp__bizgate__bizgate__prospect_suggest, mcp__bizgate__bizgate__department_search, mcp__bizgate__bizgate__company_search, AskUserQuestion, WebSearch
107
+ ---
108
+
109
+ # Prospect Find: 条件ガイド付きプロスペクト探索
110
+
111
+ 条件を対話的に確認し、ローカルDBから候補を抽出 → Claude判断 → BizGate部署検索、の3段階で営業ターゲットを発見するスキル。API消費を最小限に抑える。
112
+
113
+ ## プロセス
114
+
115
+ ### Step 1: 提案軸を確認
116
+
117
+ ユーザーの入力から **何を売りたいか(提案サービス/製品)** を特定する。
118
+ 明示されていない場合は \`AskUserQuestion\` で確認する。
119
+
120
+ \\\`\\\`\\\`
121
+ 何を提案したいですか?
122
+
123
+ 1. DOMO(BI/データ分析ダッシュボード)
124
+ 2. 営業代行(営業KPI管理・架電管理)
125
+ 3. Solution(内部管理システム・DB構築)
126
+ 4. AI(Claude × BizGate MCP連携)
127
+ 5. その他(自由入力)
128
+
129
+ 番号で選んでください。
130
+ \\\`\\\`\\\`
131
+
132
+ **提案軸 → 部署検索キーワード変換表:**
133
+
134
+ | 提案軸 | bKwd |
135
+ |--------|------|
136
+ | DOMO | 経営企画,DX,システム,情報,データ,分析,デジタル |
137
+ | 営業代行 | 営業,販売,販促,マーケティング,事業開発,顧客 |
138
+ | Solution | ICT,システム,DX,情報,企画,総務,イノベーション,デジタル |
139
+ | AI | 戦略,成長,事業創造,イノベーション,企画,開発,推進,DX,AI,ICT |
140
+
141
+ ### Step 2: 検索条件を選択
142
+
143
+ \`AskUserQuestion\` で以下の条件を**1つのメッセージで**提示する。
144
+ 不要な条件はスキップ可能であることを明記する。
145
+
146
+ \\\`\\\`\\\`
147
+ 検索条件を選んでください(不要なものはスキップOK)。
148
+
149
+ ■ データソース
150
+ a. 上場企業のみ(約3,800社 — 市場・業種で絞り込み可)
151
+ b. 全法人(約572万社 — 地域で絞り込み可)
152
+
153
+ ■ 地域(全法人の場合)
154
+ → 都道府県を入力(例: 東京都) — スキップ可
155
+
156
+ ■ 市場区分(上場企業の場合)
157
+ 1. 全市場 2. プライム 3. スタンダード 4. グロース
158
+
159
+ ■ 業種(上場企業の場合)
160
+ → キーワード入力(例: 情報・通信業) — スキップ可
161
+
162
+ ■ 取得件数
163
+ → デフォルト50社(最大200)
164
+
165
+ 例: 「a, プライム, 情報, 100社」のように回答してください。
166
+ \\\`\\\`\\\`
167
+
168
+ ### Step 3: prospect_suggest 実行(API 0回)
169
+
170
+ Step 2 の回答をパースして \`bizgate__prospect_suggest\` を呼ぶ。
171
+
172
+ - データソース a → \`source: "jpx"\` + market/industry
173
+ - データソース b → \`source: "seed"\` + pref/city
174
+
175
+ ### Step 4: Claude が候補を評価(API 0回)
176
+
177
+ 返ってきた候補リストを見て、**提案軸に合う企業を5〜10社選ぶ**。
178
+ 選定基準:
179
+ - Claudeの知識ベースで企業の規模・業種・事業内容を判断
180
+ - 提案軸のサービスを導入しそうな企業を優先
181
+ - 選定理由を簡潔に付記する
182
+
183
+ ### Step 5: department_search 実行(API N回)
184
+
185
+ 選定した企業ごとに \`bizgate__department_search\` を実行する。
186
+ - \`compno\`: 法人番号(候補リストから取得)
187
+ - \`bKwd\`: Step 1 で決めた提案軸のキーワード
188
+ - \`bKOpr\`: "0"(OR検索)
189
+
190
+ **並列実行**で効率化する(最大5社同時)。
191
+
192
+ ### Step 6: 結果出力
193
+
194
+ 企業ごとにターゲット部署・電話番号・マッチ理由を表形式で出力する。
195
+
196
+ ## 注意事項
197
+
198
+ - 電話番号がない場合は「-」と表示
199
+ - 各企業につき最大5部署まで提案
200
+ - マッチ理由は1行で簡潔に
201
+ - ユーザーの言語(日本語 or 韓国語)に合わせて出力
202
+
203
+ ---
204
+
93
205
  $ARGUMENTS
94
206
  `;
95
207
  export function main() {
208
+ // prospect-match
96
209
  const existed = existsSync(join(SKILL_DIR, "SKILL.md"));
97
210
  mkdirSync(SKILL_DIR, { recursive: true });
98
211
  writeFileSync(join(SKILL_DIR, "SKILL.md"), SKILL_MD, "utf-8");
@@ -103,10 +216,22 @@ export function main() {
103
216
  console.log("✔ prospect-match スキルをインストールしました");
104
217
  }
105
218
  console.log(` 場所: ${SKILL_DIR}/SKILL.md`);
219
+ // prospect-find
220
+ const existed2 = existsSync(join(SKILL_FIND_DIR, "SKILL.md"));
221
+ mkdirSync(SKILL_FIND_DIR, { recursive: true });
222
+ writeFileSync(join(SKILL_FIND_DIR, "SKILL.md"), SKILL_FIND_MD, "utf-8");
223
+ if (existed2) {
224
+ console.log("✔ prospect-find スキルを更新しました");
225
+ }
226
+ else {
227
+ console.log("✔ prospect-find スキルをインストールしました");
228
+ }
229
+ console.log(` 場所: ${SKILL_FIND_DIR}/SKILL.md`);
106
230
  console.log("");
107
231
  console.log("使い方:");
108
- console.log(" /prospect-match 会社名");
109
- console.log(' または「○○にDigiManのサービスが売れそうな部署を探して」');
232
+ console.log(" /prospect-match 会社名 — 特定企業の部署マッチング");
233
+ console.log(" /prospect-find DOMO — 条件ガイド付きプロスペクト探索");
234
+ console.log(' または「DOMOに合う企業を探して」「営業先を探して」');
110
235
  }
111
236
  // 直接実行された場合のみ実行
112
237
  const isDirectRun = process.argv[1]?.endsWith("install-skill.js");
@@ -12,6 +12,16 @@ export declare class JpxCache {
12
12
  constructor(jpxUrl?: string);
13
13
  init(): Promise<void>;
14
14
  isReady(): boolean;
15
+ /** 利用可能な業種一覧と件数を返す */
16
+ listIndustries(): {
17
+ industry: string;
18
+ count: number;
19
+ }[];
20
+ /** 市場別の企業数を返す */
21
+ listMarkets(): {
22
+ market: string;
23
+ count: number;
24
+ }[];
15
25
  search(params: {
16
26
  market?: string;
17
27
  industry?: string;
package/dist/jpx-cache.js CHANGED
@@ -58,6 +58,22 @@ export class JpxCache {
58
58
  isReady() {
59
59
  return this.ready && this.db !== null;
60
60
  }
61
+ /** 利用可能な業種一覧と件数を返す */
62
+ listIndustries() {
63
+ if (!this.db)
64
+ return [];
65
+ return this.db
66
+ .prepare("SELECT industry_33 as industry, COUNT(*) as count FROM listed_companies GROUP BY industry_33 ORDER BY count DESC")
67
+ .all();
68
+ }
69
+ /** 市場別の企業数を返す */
70
+ listMarkets() {
71
+ if (!this.db)
72
+ return [];
73
+ return this.db
74
+ .prepare("SELECT market, COUNT(*) as count FROM listed_companies GROUP BY market ORDER BY count DESC")
75
+ .all();
76
+ }
61
77
  search(params) {
62
78
  if (!this.db)
63
79
  return { total: 0, results: [] };
@@ -122,7 +138,10 @@ export class JpxCache {
122
138
  const codeCol = colKeys.find((k) => k.includes("コード")) ?? colKeys[0];
123
139
  const nameCol = colKeys.find((k) => k.includes("銘柄名")) ?? colKeys[1];
124
140
  const marketCol = colKeys.find((k) => k.includes("市場・商品区分")) ?? colKeys[2];
125
- const industry33Col = colKeys.find((k) => k.includes("33業種")) ?? colKeys[4];
141
+ // 33業種区分(ラベル)を優先、なければ33業種コードを使用
142
+ const industry33LabelCol = colKeys.find((k) => k.includes("33業種区分"));
143
+ const industry33CodeCol = colKeys.find((k) => k.includes("33業種コード")) ?? colKeys[4];
144
+ const industry33Col = industry33LabelCol ?? industry33CodeCol;
126
145
  console.error(`Column mapping: code=${codeCol}, name=${nameCol}, market=${marketCol}, industry=${industry33Col}`);
127
146
  // seed.db を読み書きモードで開く
128
147
  const db = new Database(DB_PATH);
@@ -175,24 +194,25 @@ export class JpxCache {
175
194
  .get();
176
195
  if (hasSeedTable) {
177
196
  console.error("Matching with seed_companies...");
178
- const listed = db
179
- .prepare("SELECT securities_code, name, name_normalized FROM listed_companies")
180
- .all();
181
- const updateCorp = db.prepare("UPDATE listed_companies SET corporate_number = ? WHERE securities_code = ?");
182
- const findSeed = db.prepare("SELECT corporate_number FROM seed_companies WHERE name LIKE ? LIMIT 1");
183
- let matched = 0;
184
- const matchBatch = db.transaction((items) => {
185
- for (const item of items) {
186
- // 正規化名で部分一致検索
187
- const seed = findSeed.get(`%${item.name_normalized}%`);
188
- if (seed) {
189
- updateCorp.run(seed.corporate_number, item.securities_code);
190
- matched++;
191
- }
192
- }
193
- });
194
- matchBatch(listed);
195
- console.error(`Matched ${matched}/${listed.length} companies with seed data`);
197
+ // 高速マッチング: 「株式会社+銘柄名」「銘柄名+株式会社」「銘柄名そのまま」で完全一致
198
+ const updateSql = `
199
+ UPDATE listed_companies SET corporate_number = (
200
+ SELECT s.corporate_number FROM seed_companies s
201
+ WHERE s.name = '株式会社' || listed_companies.name
202
+ OR s.name = listed_companies.name || '株式会社'
203
+ OR s.name = listed_companies.name
204
+ LIMIT 1
205
+ )
206
+ WHERE corporate_number IS NULL
207
+ `;
208
+ db.prepare(updateSql).run();
209
+ const matched = db
210
+ .prepare("SELECT COUNT(*) as cnt FROM listed_companies WHERE corporate_number IS NOT NULL")
211
+ .get().cnt;
212
+ const total = db
213
+ .prepare("SELECT COUNT(*) as cnt FROM listed_companies")
214
+ .get().cnt;
215
+ console.error(`Matched ${matched}/${total} companies with seed data`);
196
216
  }
197
217
  // インデックス作成
198
218
  db.exec(`
@@ -12,6 +12,11 @@ export declare class SeedCache {
12
12
  /** Initialize: check cache, download if needed, open DB */
13
13
  init(): Promise<void>;
14
14
  isReady(): boolean;
15
+ /** 利用可能な都道府県一覧と件数を返す */
16
+ listPrefectures(): {
17
+ prefecture: string;
18
+ count: number;
19
+ }[];
15
20
  /** Search seed companies */
16
21
  search(params: {
17
22
  pref?: string;
@@ -43,6 +43,14 @@ export class SeedCache {
43
43
  isReady() {
44
44
  return this.ready && this.db !== null;
45
45
  }
46
+ /** 利用可能な都道府県一覧と件数を返す */
47
+ listPrefectures() {
48
+ if (!this.db)
49
+ return [];
50
+ return this.db
51
+ .prepare("SELECT prefecture, COUNT(*) as count FROM seed_companies WHERE prefecture IS NOT NULL AND prefecture != '' GROUP BY prefecture ORDER BY count DESC")
52
+ .all();
53
+ }
46
54
  /** Search seed companies */
47
55
  search(params) {
48
56
  if (!this.db)
@@ -122,8 +130,8 @@ export class SeedCache {
122
130
  const lines = csv.split("\n");
123
131
  const batchInsert = db.transaction((rows) => {
124
132
  for (const row of rows) {
125
- if (row.length >= 4) {
126
- insert.run(row[0], row[1], row[2], row[3]);
133
+ if (row.length >= 6) {
134
+ insert.run(row[0], row[1], row[4], row[5]);
127
135
  }
128
136
  }
129
137
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bizgate-mcp-server",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "BizGate APIとClaudeを連携するMCPサーバー",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",