bizgate-mcp-server 0.3.6 → 0.3.7

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.
Files changed (2) hide show
  1. package/dist/index.js +144 -55
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ if (authMode === "ip" && !process.env.BIZGATE_APP) {
37
37
  process.exit(1);
38
38
  }
39
39
  const dailyLimit = Number(process.env.BIZGATE_DAILY_LIMIT ?? "200");
40
- const seedApiUrl = process.env.BIZGATE_SEED_API_URL ?? "";
40
+ const seedApiUrl = process.env.SEED_API_URL ?? "http://192.168.40.94:3456";
41
41
  const usageFile = process.env.BIZGATE_USAGE_FILE ??
42
42
  join(homedir(), ".bizgate-mcp-usage.json");
43
43
  const baseUrl = process.env.BIZGATE_BASE_URL ??
@@ -422,16 +422,6 @@ server.tool("bizgate__usage_status", "本日のBizGate API残り利用回数を
422
422
  };
423
423
  });
424
424
  // ---------- Tool 7: プロスペクトリスト(シードAPI連携) ----------
425
- const REVENUE_CATEGORIES = {
426
- 1: "1. 1000万未満",
427
- 2: "2. 1000万-3000万未満",
428
- 3: "3. 3000万-1億未満",
429
- 4: "4. 1億-5億未満",
430
- 5: "5. 5億-10億未満",
431
- 6: "6. 10億-20億未満",
432
- 7: "7. 20億-100億未満",
433
- 8: "8. 100億以上",
434
- };
435
425
  const MAX_API_CALLS_HARD_LIMIT = 150;
436
426
  async function fetchSeeds(params) {
437
427
  const url = new URL("/search", seedApiUrl);
@@ -442,45 +432,67 @@ async function fetchSeeds(params) {
442
432
  throw new Error(`seed-api エラー: HTTP ${res.status}`);
443
433
  return (await res.json());
444
434
  }
445
- function revenueMatchesMin(revenue, minCategory) {
435
+ /** 売上カテゴリ文字列から数値を抽出して最低ラインと比較 */
436
+ function revenueMatchesMin(revenue, minLabel) {
446
437
  if (!revenue)
447
438
  return false;
439
+ // revenue例: "7. 20億-100億未満", minLabel例: "50億以上"
440
+ const thresholds = {
441
+ "50億以上": 7, // カテゴリ7(20億-100億) 以上
442
+ "300億以上": 8, // カテゴリ8(100億以上)
443
+ "1000億以上": 8, // カテゴリ8(100億以上)
444
+ };
445
+ const minCat = thresholds[minLabel];
446
+ if (!minCat)
447
+ return true; // 不明なラベルはフィルタしない
448
448
  const match = revenue.match(/^(\d+)\./);
449
449
  if (!match)
450
450
  return false;
451
- return Number(match[1]) >= minCategory;
451
+ return Number(match[1]) >= minCat;
452
452
  }
453
- server.tool("bizgate__prospect_list", "シードリスト(国税庁法人データ)から条件に合う企業を抽出し、BizGate APIで詳細を照会してプロスペクトリストを作成する。BIZGATE_SEED_API_URLの設定が必要。", {
453
+ /** proposal_axis からアプローチキーワードを生成 */
454
+ function axisToKeywords(axis) {
455
+ const axes = Array.isArray(axis) ? axis : [axis];
456
+ const keywordMap = {
457
+ "DOMO": "経営企画,DX,システム,情報,データ,分析",
458
+ "営業DX/CRM": "営業,マーケティング,販売,顧客,CRM,販促",
459
+ "営業DX": "営業,マーケティング,販売,顧客,CRM,販促",
460
+ "CRM": "営業,マーケティング,顧客,CRM",
461
+ };
462
+ const keywords = axes.flatMap((a) => (keywordMap[a] ?? a).split(","));
463
+ return [...new Set(keywords)].join(",");
464
+ }
465
+ server.tool("bizgate__prospect_list", "シードリスト(国税庁572万法人)から条件に合う企業を抽出し、BizGate APIで部署・キーマンまで照会してプロスペクトリストを作成する。1回の実行で約40 APIコールを消費(1日約5回実行可能)。SEED_API_URLが必要。", {
454
466
  pref: z.string().optional().describe("都道府県(例: 東京都)"),
455
- keyword: z.string().optional().describe("会社名キーワード(部分一致)"),
456
- industry: z.string().optional().describe("業種キーワード(例: IT,製造)。BizGateの業種情報でフィルタリング"),
457
- minRevenue: z.number().optional().describe("最低売上カテゴリ番号(1=1000万未満, 2=1000万-3000万, 3=3000万-1億, 4=1億-5億, 5=5億-10億, 6=10億-20億, 7=20億-100億, 8=100億以上)"),
458
- targetCount: z.number().optional().describe("取得したい企業数(デフォルト: 10、最大: 20)"),
459
- maxApiCalls: z.number().optional().describe("このツール1回で使うAPI上限(デフォルト: 50、最大: 150)"),
460
- }, async ({ pref, keyword, industry, minRevenue, targetCount: rawTarget, maxApiCalls: rawMax }) => {
467
+ city: z.string().optional().describe("市区町村名(例: 千代田区)"),
468
+ industry: z.string().optional().describe("BizGate業種分類キーワード(例: 製造業, サービス, IT"),
469
+ revenue_min: z.string().optional().describe("最低売上(50億以上 / 300億以上 / 1000億以上)"),
470
+ employee_500plus: z.boolean().optional().describe("従業員500人以上のみ"),
471
+ proposal_axis: z.union([z.string(), z.array(z.string())]).optional().describe("提案軸(DOMO / 営業DX/CRM)。部署検索のキーワードに変換される"),
472
+ count: z.number().optional().describe("取得したい企業数(デフォルト: 10、最大: 30)"),
473
+ }, async ({ pref, city, industry, revenue_min, employee_500plus, proposal_axis, count: rawCount }) => {
461
474
  if (!seedApiUrl) {
462
475
  return {
463
- content: [{ type: "text", text: "エラー: BIZGATE_SEED_API_URL が設定されていません。管理者に連絡してください。" }],
476
+ content: [{ type: "text", text: "エラー: SEED_API_URL が設定されていません。管理者に連絡してください。" }],
464
477
  };
465
478
  }
466
- const targetCount = Math.min(rawTarget ?? 10, 20);
467
- const apiLimit = Math.min(rawMax ?? 50, MAX_API_CALLS_HARD_LIMIT);
479
+ const count = Math.min(rawCount ?? 10, 30);
468
480
  const remaining = usageTracker.remaining();
469
- const effectiveLimit = Math.min(apiLimit, remaining);
470
- if (effectiveLimit < 5) {
481
+ if (remaining < 10) {
471
482
  return {
472
- content: [{ type: "text", text: `エラー: 残りAPI回数が${remaining}回しかありません。明日以降にお試しください。${usageFooter()}` }],
483
+ content: [{ type: "text", text: `エラー: 残りAPI回数が${remaining}回です。明日以降にお試しください。${usageFooter()}` }],
473
484
  };
474
485
  }
475
- // Step 1: シードAPIから候補取得
486
+ // ---- Step 1: シードAPIから候補取得 (API 0回) ----
476
487
  const seedParams = {
477
- limit: String(targetCount * 5),
488
+ limit: "200",
478
489
  shuffle: "true",
490
+ type: "株式会社",
479
491
  };
480
492
  if (pref)
481
493
  seedParams.pref = pref;
482
- if (keyword)
483
- seedParams.keyword = keyword;
494
+ if (city)
495
+ seedParams.city = city;
484
496
  let seeds;
485
497
  let seedTotal;
486
498
  try {
@@ -490,7 +502,7 @@ server.tool("bizgate__prospect_list", "シードリスト(国税庁法人デ
490
502
  }
491
503
  catch (e) {
492
504
  return {
493
- content: [{ type: "text", text: `シードAPIへの接続に失敗しました: ${e instanceof Error ? e.message : "不明なエラー"}` }],
505
+ content: [{ type: "text", text: `seed-api に接続できません(${seedApiUrl}): ${e instanceof Error ? e.message : "不明なエラー"}` }],
494
506
  };
495
507
  }
496
508
  if (seeds.length === 0) {
@@ -498,17 +510,16 @@ server.tool("bizgate__prospect_list", "シードリスト(国税庁法人デ
498
510
  content: [{ type: "text", text: `条件に合う企業がシードリストに見つかりませんでした。${usageFooter()}` }],
499
511
  };
500
512
  }
501
- // Step 2: BizGate APIで詳細照会 + フィルタリング
502
- const matched = [];
513
+ // ---- Step 2: BizGate企業検索 売上/業種/従業員フィルタ ----
514
+ const verified = [];
503
515
  let apiCalls = 0;
504
516
  const batchSize = 5;
505
- for (let i = 0; i < seeds.length && matched.length < targetCount && apiCalls < effectiveLimit; i += batchSize) {
506
- const batch = seeds.slice(i, Math.min(i + batchSize, seeds.length));
507
- const remaining2 = effectiveLimit - apiCalls;
508
- const actualBatch = batch.slice(0, remaining2);
509
- const results = await Promise.all(actualBatch.map(async (seed) => {
517
+ const targetVerified = Math.ceil(count * 1.5); // 部署/キーマンで脱落分を見越す
518
+ for (let i = 0; i < seeds.length && verified.length < targetVerified && apiCalls < MAX_API_CALLS_HARD_LIMIT; i += batchSize) {
519
+ const batch = seeds.slice(i, i + batchSize);
520
+ const results = await Promise.all(batch.map(async (seed) => {
510
521
  try {
511
- const { docs } = await client.searchCompany({ compno: seed.compno });
522
+ const { docs } = await client.searchCompany({ compno: seed.corporate_number });
512
523
  apiCalls++;
513
524
  return { seed, doc: docs[0] ?? null };
514
525
  }
@@ -520,37 +531,115 @@ server.tool("bizgate__prospect_list", "シードリスト(国税庁法人デ
520
531
  for (const { seed, doc } of results) {
521
532
  if (!doc)
522
533
  continue;
523
- // 売上フィルタ
524
- if (minRevenue && !revenueMatchesMin(doc.revenue, minRevenue))
534
+ if (revenue_min && !revenueMatchesMin(doc.revenue, revenue_min))
525
535
  continue;
526
- // 業種フィルタ
527
536
  if (industry) {
528
537
  const docIndustry = f(doc.gyoshu_facet);
529
- const keywords = industry.split(",").map((k) => k.trim());
530
- if (!keywords.some((k) => docIndustry.includes(k)))
538
+ if (!industry.split(",").some((k) => docIndustry.includes(k.trim())))
531
539
  continue;
532
540
  }
533
- matched.push({ doc, seed });
541
+ if (employee_500plus && doc.emp) {
542
+ const empMatch = doc.emp.match(/(\d+)/);
543
+ if (empMatch) {
544
+ // emp例: "6. 500-1000人未満" → カテゴリ6以上
545
+ const empCat = Number(doc.emp.match(/^(\d+)\./)?.[1] ?? 0);
546
+ if (empCat < 6)
547
+ continue; // カテゴリ6 = 500-1000人
548
+ }
549
+ }
550
+ verified.push({ doc, seed });
534
551
  }
535
552
  }
536
- // Step 3: 結果フォーマット
537
- if (matched.length === 0) {
553
+ if (verified.length === 0) {
538
554
  return {
539
555
  content: [{
540
556
  type: "text",
541
- text: `条件に合う企業が見つかりませんでした。\n(シード候補: ${seeds.length}社 / API照会: ${apiCalls}回 / シード全体: ${seedTotal}社)${usageFooter()}`,
557
+ text: `条件に合う企業が見つかりませんでした。\n(シード候補: ${seeds.length}社 / API照会: ${apiCalls}回)${usageFooter()}`,
542
558
  }],
543
559
  };
544
560
  }
545
- const header = `## プロスペクトリスト(${matched.length}社)\n\n`;
546
- const table = `| # | 会社名 | 業種 | 売上 | 従業員 | 所在地 | HP |\n|---|--------|------|------|--------|--------|----|\n` +
547
- matched
548
- .map((m, i) => {
549
- const d = m.doc;
550
- return `| ${i + 1} | ${f(d.shogo)} | ${f(d.gyoshu_facet)} | ${d.revenue ?? "-"} | ${d.emp ?? "-"} | ${f(d.add)} | ${f(d.hpurl) || "-"} |`;
561
+ // ---- Step 3: 部署 + キーマン検索 ----
562
+ const deptKeywords = proposal_axis ? axisToKeywords(proposal_axis) : "";
563
+ const prospects = [];
564
+ for (const { doc, seed } of verified.slice(0, count)) {
565
+ const companyName = f(doc.shogo);
566
+ let department = "-";
567
+ let phone = f(doc.tel) || "-";
568
+ let keyman;
569
+ // 部署検索
570
+ if (deptKeywords) {
571
+ try {
572
+ const { docs: deptDocs } = await client.searchDepartments({
573
+ shogo: companyName, bKwd: deptKeywords, bKOpr: "0",
574
+ });
575
+ apiCalls++;
576
+ if (deptDocs.length > 0) {
577
+ department = f(deptDocs[0].bumon) || "-";
578
+ if (f(deptDocs[0].tel))
579
+ phone = f(deptDocs[0].tel);
580
+ }
581
+ }
582
+ catch {
583
+ apiCalls++;
584
+ }
585
+ }
586
+ // キーマン検索
587
+ if (skeyKeyman && skeyKeymanName) {
588
+ try {
589
+ const { docs: keyDocs } = await client.searchKeymanWithNames({
590
+ shogo: companyName, bKwd: deptKeywords || undefined, limit: 1,
591
+ });
592
+ apiCalls += 2; // 一覧1回 + 詳細1回
593
+ if (keyDocs.length > 0) {
594
+ keyman = {
595
+ name: f(keyDocs[0].ceo) || "(不明)",
596
+ title: f(keyDocs[0].bumon) || "(不明)",
597
+ };
598
+ if (f(keyDocs[0].tel))
599
+ phone = f(keyDocs[0].tel);
600
+ }
601
+ }
602
+ catch {
603
+ apiCalls += 1;
604
+ }
605
+ }
606
+ // アプローチノート生成
607
+ const axes = proposal_axis
608
+ ? (Array.isArray(proposal_axis) ? proposal_axis : [proposal_axis])
609
+ : [];
610
+ const axisLabel = axes.join("・") || "汎用";
611
+ const approachNote = `${axisLabel}の導入提案。${f(doc.gyoshu_facet) || ""}${doc.emp ? `、${doc.emp}` : ""}。`;
612
+ prospects.push({
613
+ name: companyName,
614
+ prefecture: seed.prefecture,
615
+ city: seed.city,
616
+ corporate_number: seed.corporate_number,
617
+ industry: f(doc.gyoshu_facet) || "-",
618
+ revenue: doc.revenue ?? "-",
619
+ department,
620
+ keyman,
621
+ phone,
622
+ approach_note: approachNote,
623
+ });
624
+ if (apiCalls >= MAX_API_CALLS_HARD_LIMIT)
625
+ break;
626
+ }
627
+ // ---- Step 4: 結果フォーマット ----
628
+ // キーマンあり > 部署あり > 会社のみ でソート
629
+ prospects.sort((a, b) => {
630
+ const scoreA = (a.keyman ? 2 : 0) + (a.department !== "-" ? 1 : 0);
631
+ const scoreB = (b.keyman ? 2 : 0) + (b.department !== "-" ? 1 : 0);
632
+ return scoreB - scoreA;
633
+ });
634
+ const header = `## プロスペクトリスト(${prospects.length}社)\n\n`;
635
+ const table = `| # | 会社名 | 部署 | キーマン | 電話 | アプローチ角度 |\n|---|--------|------|---------|------|---------------|\n` +
636
+ prospects
637
+ .map((p, i) => {
638
+ const keymanStr = p.keyman ? `${p.keyman.name}(${p.keyman.title})` : "-";
639
+ return `| ${i + 1} | ${p.name} | ${p.department} | ${keymanStr} | ${p.phone} | ${p.approach_note} |`;
551
640
  })
552
641
  .join("\n");
553
- const stats = `\n\n---\nシード候補: ${seeds.length}社(全${seedTotal}社中) / API照会: ${apiCalls}回 / マッチ: ${matched.length}社`;
642
+ const stats = `\n\n---\nシード: ${seedTotal}社中${seeds.length}社取得 / API照会: ${apiCalls}回 / マッチ: ${prospects.length}社`;
554
643
  return {
555
644
  content: [{ type: "text", text: header + table + stats + usageFooter() }],
556
645
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bizgate-mcp-server",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "BizGate APIとClaudeを連携するMCPサーバー",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",