bizgate-mcp-server 0.3.5 → 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 +228 -8
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -37,6 +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.SEED_API_URL ?? "http://192.168.40.94:3456";
40
41
  const usageFile = process.env.BIZGATE_USAGE_FILE ??
41
42
  join(homedir(), ".bizgate-mcp-usage.json");
42
43
  const baseUrl = process.env.BIZGATE_BASE_URL ??
@@ -204,7 +205,7 @@ server.tool("bizgate__company_search", "会社名または法人番号で企業
204
205
  }
205
206
  });
206
207
  // ---------- Tool 2: 部署検索 ----------
207
- server.tool("bizgate__department_search", "部署情報(部署名・住所・電話番号・カテゴリ)を検索する。会社名・法人番号での検索に加え、部署名キーワード(bKwd)やカテゴリ(cList)のみでの全企業横断検索も可能。最大500件。デフォルトで上位30件を表示(limitで変更可能)。", {
208
+ server.tool("bizgate__department_search", "会社名または法人番号で部署情報(部署名・住所・電話番号・カテゴリ)を検索する。最大500件。都道府県・カテゴリ・部署名キーワードで絞り込み可能。デフォルトで上位30件を表示(limitで変更可能)。", {
208
209
  shogo: z.string().optional().describe("会社名(例:株式会社○○)"),
209
210
  compno: z.string().optional().describe("法人番号(13桁)"),
210
211
  pList: z.string().optional().describe("都道府県コード(カンマ区切り。例: 13,14 = 東京都,神奈川県)"),
@@ -213,13 +214,9 @@ server.tool("bizgate__department_search", "部署情報(部署名・住所・
213
214
  bKOpr: z.string().optional().describe("キーワード結合演算子(0=OR(デフォルト), 1=AND)"),
214
215
  limit: z.number().optional().describe("表示する件数の上限(デフォルト: 30)"),
215
216
  }, async ({ shogo, compno, pList, cList, bKwd, bKOpr, limit }) => {
216
- // 部署検索: bKwd or cList があれば会社名なしでも検索可能
217
- if (!shogo && !compno && !bKwd && !cList) {
218
- return { content: [{ type: "text", text: "エラー: 会社名(shogo)、法人番号(compno)、部署名キーワード(bKwd)、カテゴリ(cList)のいずれかを入力してください。" }] };
219
- }
220
- if (compno && !/^\d{13}$/.test(compno)) {
221
- return { content: [{ type: "text", text: "エラー: 法人番号は13桁の数字で入力してください。" }] };
222
- }
217
+ const err = validateInput(shogo, compno);
218
+ if (err)
219
+ return { content: [{ type: "text", text: err }] };
223
220
  const displayLimit = limit ?? 30;
224
221
  try {
225
222
  const { docs, numFound } = await client.searchDepartments({
@@ -424,6 +421,229 @@ server.tool("bizgate__usage_status", "本日のBizGate API残り利用回数を
424
421
  ],
425
422
  };
426
423
  });
424
+ // ---------- Tool 7: プロスペクトリスト(シードAPI連携) ----------
425
+ const MAX_API_CALLS_HARD_LIMIT = 150;
426
+ async function fetchSeeds(params) {
427
+ const url = new URL("/search", seedApiUrl);
428
+ for (const [k, v] of Object.entries(params))
429
+ url.searchParams.set(k, v);
430
+ const res = await fetch(url);
431
+ if (!res.ok)
432
+ throw new Error(`seed-api エラー: HTTP ${res.status}`);
433
+ return (await res.json());
434
+ }
435
+ /** 売上カテゴリ文字列から数値を抽出して最低ラインと比較 */
436
+ function revenueMatchesMin(revenue, minLabel) {
437
+ if (!revenue)
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
+ const match = revenue.match(/^(\d+)\./);
449
+ if (!match)
450
+ return false;
451
+ return Number(match[1]) >= minCat;
452
+ }
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が必要。", {
466
+ pref: z.string().optional().describe("都道府県(例: 東京都)"),
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 }) => {
474
+ if (!seedApiUrl) {
475
+ return {
476
+ content: [{ type: "text", text: "エラー: SEED_API_URL が設定されていません。管理者に連絡してください。" }],
477
+ };
478
+ }
479
+ const count = Math.min(rawCount ?? 10, 30);
480
+ const remaining = usageTracker.remaining();
481
+ if (remaining < 10) {
482
+ return {
483
+ content: [{ type: "text", text: `エラー: 残りAPI回数が${remaining}回です。明日以降にお試しください。${usageFooter()}` }],
484
+ };
485
+ }
486
+ // ---- Step 1: シードAPIから候補取得 (API 0回) ----
487
+ const seedParams = {
488
+ limit: "200",
489
+ shuffle: "true",
490
+ type: "株式会社",
491
+ };
492
+ if (pref)
493
+ seedParams.pref = pref;
494
+ if (city)
495
+ seedParams.city = city;
496
+ let seeds;
497
+ let seedTotal;
498
+ try {
499
+ const result = await fetchSeeds(seedParams);
500
+ seeds = result.results;
501
+ seedTotal = result.total;
502
+ }
503
+ catch (e) {
504
+ return {
505
+ content: [{ type: "text", text: `seed-api に接続できません(${seedApiUrl}): ${e instanceof Error ? e.message : "不明なエラー"}` }],
506
+ };
507
+ }
508
+ if (seeds.length === 0) {
509
+ return {
510
+ content: [{ type: "text", text: `条件に合う企業がシードリストに見つかりませんでした。${usageFooter()}` }],
511
+ };
512
+ }
513
+ // ---- Step 2: BizGate企業検索 → 売上/業種/従業員フィルタ ----
514
+ const verified = [];
515
+ let apiCalls = 0;
516
+ const batchSize = 5;
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) => {
521
+ try {
522
+ const { docs } = await client.searchCompany({ compno: seed.corporate_number });
523
+ apiCalls++;
524
+ return { seed, doc: docs[0] ?? null };
525
+ }
526
+ catch {
527
+ apiCalls++;
528
+ return { seed, doc: null };
529
+ }
530
+ }));
531
+ for (const { seed, doc } of results) {
532
+ if (!doc)
533
+ continue;
534
+ if (revenue_min && !revenueMatchesMin(doc.revenue, revenue_min))
535
+ continue;
536
+ if (industry) {
537
+ const docIndustry = f(doc.gyoshu_facet);
538
+ if (!industry.split(",").some((k) => docIndustry.includes(k.trim())))
539
+ continue;
540
+ }
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 });
551
+ }
552
+ }
553
+ if (verified.length === 0) {
554
+ return {
555
+ content: [{
556
+ type: "text",
557
+ text: `条件に合う企業が見つかりませんでした。\n(シード候補: ${seeds.length}社 / API照会: ${apiCalls}回)${usageFooter()}`,
558
+ }],
559
+ };
560
+ }
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} |`;
640
+ })
641
+ .join("\n");
642
+ const stats = `\n\n---\nシード: ${seedTotal}社中${seeds.length}社取得 / API照会: ${apiCalls}回 / マッチ: ${prospects.length}社`;
643
+ return {
644
+ content: [{ type: "text", text: header + table + stats + usageFooter() }],
645
+ };
646
+ });
427
647
  // ---------- 起動 ----------
428
648
  async function main() {
429
649
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bizgate-mcp-server",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "BizGate APIとClaudeを連携するMCPサーバー",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",