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.
- package/dist/index.js +228 -8
- 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", "
|
|
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
|
-
|
|
217
|
-
if (
|
|
218
|
-
return { content: [{ type: "text", text:
|
|
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();
|