bizgate-mcp-server 0.3.10 → 0.4.1

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 (38) hide show
  1. package/README.md +82 -2
  2. package/dist/cloud/neon-client.d.ts +37 -0
  3. package/dist/cloud/neon-client.js +136 -0
  4. package/dist/cloud/r2-client.d.ts +25 -0
  5. package/dist/cloud/r2-client.js +97 -0
  6. package/dist/index.d.ts +5 -1
  7. package/dist/index.js +101 -2
  8. package/dist/papatto/credentials.d.ts +12 -0
  9. package/dist/papatto/credentials.js +40 -0
  10. package/dist/papatto/csv-parser.d.ts +9 -0
  11. package/dist/papatto/csv-parser.js +134 -0
  12. package/dist/papatto/friendly-errors.d.ts +8 -0
  13. package/dist/papatto/friendly-errors.js +103 -0
  14. package/dist/papatto/master-extractor.d.ts +51 -0
  15. package/dist/papatto/master-extractor.js +295 -0
  16. package/dist/papatto/page-extract.d.ts +55 -0
  17. package/dist/papatto/page-extract.js +324 -0
  18. package/dist/papatto/pipeline.d.ts +67 -0
  19. package/dist/papatto/pipeline.js +295 -0
  20. package/dist/papatto/playwright-session.d.ts +11 -0
  21. package/dist/papatto/playwright-session.js +85 -0
  22. package/dist/papatto/search.d.ts +72 -0
  23. package/dist/papatto/search.js +512 -0
  24. package/dist/papatto/tools.d.ts +2 -0
  25. package/dist/papatto/tools.js +770 -0
  26. package/dist/prospect-history.d.ts +63 -0
  27. package/dist/prospect-history.js +155 -0
  28. package/dist/queue/playwright-queue.d.ts +9 -0
  29. package/dist/queue/playwright-queue.js +23 -0
  30. package/dist/shared/env.d.ts +1 -0
  31. package/dist/shared/env.js +40 -0
  32. package/dist/shared/logger.d.ts +2 -0
  33. package/dist/shared/logger.js +46 -0
  34. package/dist/shared/retry.d.ts +8 -0
  35. package/dist/shared/retry.js +20 -0
  36. package/dist/sheets/client.d.ts +15 -0
  37. package/dist/sheets/client.js +128 -0
  38. package/package.json +9 -2
package/README.md CHANGED
@@ -60,8 +60,8 @@ Mac の場合:
60
60
 
61
61
  ```smalltalk
62
62
  claude mcp add bizgate --scope user \
63
- -e "BIZGATE_USERNAME=digi-man_bizg1" \
64
- -e "BIZGATE_PASSWORD=digi-man_bizg1" \
63
+ -e "BIZGATE_USERNAME=digi-man_testbizg1" \
64
+ -e "BIZGATE_PASSWORD=digi-man_testbizg1" \
65
65
  -e "BIZGATE_AUTH_MODE=basic" \
66
66
  -e "BIZGATE_DAILY_LIMIT=200" \
67
67
  -e "BIZGATE_SKEY_COMPANY=/EhMJ9YMCJtJgo73.DjLuew8rnnTlb.F6/MuiESFXmZwlCKvG8bMm" \
@@ -235,3 +235,83 @@ bash install-skill.sh
235
235
 
236
236
  検索条件が曖昧で、複数の企業がヒットしています。
237
237
  会社名をより正確に入力するか、メールアドレスやホームページURLを追加で伝えてください。
238
+
239
+ ---
240
+
241
+ ## v0.4.0 — Papatto Cloud 連携 (NEW)
242
+
243
+ 自然言語の ICP (理想顧客像) を受け取って、Papatto Cloud から自動で企業リストを抽出し、CSV → R2 → Neon → Google Sheets まで一気通貫で実行します。
244
+
245
+ ```
246
+ [Slack DM / Claude Code]
247
+ "DOMO 営業 IT, 売上 50億以上, 展示会出展した 100社"
248
+
249
+ [papatto-prospect スキル]
250
+ ↓ master_summary 参照 → 条件マッピング
251
+ [papatto__extract]
252
+ ↓ Playwright 自動ログイン + 検索 + CSV ダウンロード
253
+ [R2 papatto-bizgate-logs] ← CSV 保存
254
+ [Neon papatto.campaigns] ← メタ記録
255
+
256
+ [papatto__export_to_sheets]
257
+
258
+ [Google Sheets] ← 行データ書き込み
259
+
260
+ 返答: シート URL + campaign_id
261
+ ```
262
+
263
+ ### 追加 MCP ツール (7 個)
264
+
265
+ | ツール | 用途 |
266
+ |--------|------|
267
+ | `papatto__credential_set` | Papatto 認証情報を OS Keychain に保存 + Neon users へ upsert |
268
+ | `papatto__master_refresh` | 検索画面の選択肢 (業種 / マーケタグ / インテント) をマスタ化 |
269
+ | `papatto__master_summary` | LLM が自然言語マッピングするための分類別オプション JSON |
270
+ | `papatto__extract` | 構造化 conditions → 検索 → 件数確認 → CSV → R2 → Neon |
271
+ | `papatto__export_to_sheets` | CSV → 新規 / 既存スプレッドシート + タブ |
272
+ | `papatto__campaign_status` | キャンペーン状態照会 (単件 / オーナー別リスト) |
273
+ | `papatto__queue_status` | Playwright 並列実行キューの状態 |
274
+
275
+ ### 追加スキル
276
+
277
+ - **`papatto-prospect`** — 自然言語 ICP → conditions → extract → sheets の対話フロー
278
+
279
+ ### 環境変数 (Papatto 部分)
280
+
281
+ `.env` (リポジトリルート) に以下を追加:
282
+
283
+ ```bash
284
+ # Cloudflare R2 (papatto-bizgate-logs)
285
+ R2_ACCESS_KEY_ID=...
286
+ R2_SECRET_ACCESS_KEY=...
287
+ R2_BUCKET=papatto-bizgate-logs
288
+ R2_ENDPOINT=https://<account_id>.r2.cloudflarestorage.com
289
+
290
+ # Neon (digiman-internal-db / papatto_mcp role)
291
+ NEON_DATABASE_URL=postgresql://papatto_mcp:<pw>@<host>/neondb?sslmode=require
292
+
293
+ # Google Sheets (サービスアカウント)
294
+ GOOGLE_SHEETS_CREDENTIALS_PATH=/Users/<you>/.papatto-bizgate/google_credentials.json
295
+ GOOGLE_SHEETS_DEFAULT_FOLDER_ID=<Drive folder id>
296
+
297
+ # Playwright (任意)
298
+ PAPATTO_PLAYWRIGHT_CONCURRENCY=2
299
+ ```
300
+
301
+ > Papatto 認証情報はサーバ起動時の `.env` には入れず、`papatto__credential_set` 経由で OS Keychain (keytar) に保存されます。
302
+
303
+ ### セットアップ
304
+
305
+ 詳細は [`docs/setup-mac-mini.md`](docs/setup-mac-mini.md) を参照。
306
+ ユーザー向け Slack コマンド一覧は [`docs/slack-commands.md`](docs/slack-commands.md)。
307
+
308
+ ### Papatto サイト構造のメモ (実装根拠)
309
+
310
+ - ベース URL: `https://www.papatto.info/papatto/papatto.php` — SPA 単一ページ、6 タブ
311
+ - 業種 = `chk_gyoshu[]` 18 大分類 + 個別 input 93 中分類
312
+ - マーケ / インテント / 活動 / トレンド / 分野タグ = `.kw_group a[data-query]` アンカー (`<a onclick>` ではない)
313
+ - 検索ボタン = `Papatto検索`
314
+ - CSV / Excel ボタン = `<span>:has-text("CSVダウンロード")`
315
+ - 件数 = `N件 ( 会社数 M 社 )` 正規表現で抽出
316
+ - 都道府県 / 売上 / 従業員レンジは検索フォームに無く、結果ページ facet drill または事前定義タグ (`2146_売上50億以上` 等) で代用
317
+ - ダウンロード月間上限 (例: 4,000 社) を結果ページから自動感知
@@ -0,0 +1,37 @@
1
+ import { type PoolClient } from "pg";
2
+ export declare function isConfigured(): boolean;
3
+ export declare function ensureSchema(): Promise<void>;
4
+ export declare function withTx<T>(fn: (c: PoolClient) => Promise<T>): Promise<T>;
5
+ export declare function upsertUser(args: {
6
+ slack_user_id: string;
7
+ papatto_email: string;
8
+ display_name?: string;
9
+ }): Promise<void>;
10
+ export interface CampaignRow {
11
+ campaign_id: string;
12
+ owner_slack_user_id: string;
13
+ natural_language_query: string | null;
14
+ papatto_conditions: unknown;
15
+ extracted_count: number | null;
16
+ status: "extracting" | "done" | "failed" | string;
17
+ created_at: number;
18
+ updated_at: number;
19
+ r2_extracted_url: string | null;
20
+ sheet_url: string | null;
21
+ }
22
+ export declare function insertCampaign(args: {
23
+ campaign_id: string;
24
+ owner_slack_user_id: string;
25
+ natural_language_query?: string;
26
+ papatto_conditions?: unknown;
27
+ }): Promise<void>;
28
+ export declare function updateCampaign(args: {
29
+ campaign_id: string;
30
+ status?: "extracting" | "done" | "failed";
31
+ extracted_count?: number;
32
+ r2_extracted_url?: string;
33
+ sheet_url?: string;
34
+ }): Promise<void>;
35
+ export declare function getCampaign(campaign_id: string): Promise<CampaignRow | null>;
36
+ export declare function listCampaignsByOwner(owner_slack_user_id: string, limit?: number): Promise<CampaignRow[]>;
37
+ export declare function closePool(): Promise<void>;
@@ -0,0 +1,136 @@
1
+ import { Pool } from "pg";
2
+ import { readFileSync } from "node:fs";
3
+ import { join, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { log } from "../shared/logger.js";
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const MIGRATION_SQL_PATH = join(__dirname, "..", "..", "scripts", "migrate-neon.sql");
8
+ let cachedPool = null;
9
+ let schemaReady = false;
10
+ export function isConfigured() {
11
+ return !!process.env.NEON_DATABASE_URL;
12
+ }
13
+ function pool() {
14
+ if (!cachedPool) {
15
+ const url = process.env.NEON_DATABASE_URL;
16
+ if (!url)
17
+ throw new Error("NEON_DATABASE_URL 환경변수가 설정되지 않았습니다");
18
+ cachedPool = new Pool({
19
+ connectionString: url,
20
+ max: 4,
21
+ idleTimeoutMillis: 30_000,
22
+ ssl: url.includes("sslmode=") ? undefined : { rejectUnauthorized: false },
23
+ });
24
+ cachedPool.on("error", (err) => log("error", "neon pool error", { error: String(err) }));
25
+ }
26
+ return cachedPool;
27
+ }
28
+ export async function ensureSchema() {
29
+ if (schemaReady)
30
+ return;
31
+ if (!isConfigured())
32
+ return;
33
+ const sql = readFileSync(MIGRATION_SQL_PATH, "utf-8");
34
+ await pool().query(sql);
35
+ schemaReady = true;
36
+ log("info", "neon schema ensured");
37
+ }
38
+ export async function withTx(fn) {
39
+ const client = await pool().connect();
40
+ try {
41
+ await client.query("BEGIN");
42
+ const result = await fn(client);
43
+ await client.query("COMMIT");
44
+ return result;
45
+ }
46
+ catch (e) {
47
+ await client.query("ROLLBACK").catch(() => undefined);
48
+ throw e;
49
+ }
50
+ finally {
51
+ client.release();
52
+ }
53
+ }
54
+ export async function upsertUser(args) {
55
+ await ensureSchema();
56
+ const now = Date.now();
57
+ await pool().query(`INSERT INTO papatto.users (slack_user_id, papatto_email, display_name, created_at, updated_at)
58
+ VALUES ($1, $2, $3, $4, $4)
59
+ ON CONFLICT (slack_user_id) DO UPDATE
60
+ SET papatto_email = EXCLUDED.papatto_email,
61
+ display_name = COALESCE(EXCLUDED.display_name, papatto.users.display_name),
62
+ updated_at = EXCLUDED.updated_at`, [args.slack_user_id, args.papatto_email, args.display_name ?? null, now]);
63
+ }
64
+ export async function insertCampaign(args) {
65
+ await ensureSchema();
66
+ const now = Date.now();
67
+ await pool().query(`INSERT INTO papatto.campaigns
68
+ (campaign_id, owner_slack_user_id, natural_language_query, papatto_conditions,
69
+ status, created_at, updated_at)
70
+ VALUES ($1, $2, $3, $4, 'extracting', $5, $5)
71
+ ON CONFLICT (campaign_id) DO UPDATE
72
+ SET natural_language_query = COALESCE(EXCLUDED.natural_language_query, papatto.campaigns.natural_language_query),
73
+ papatto_conditions = COALESCE(EXCLUDED.papatto_conditions, papatto.campaigns.papatto_conditions),
74
+ updated_at = EXCLUDED.updated_at`, [
75
+ args.campaign_id,
76
+ args.owner_slack_user_id,
77
+ args.natural_language_query ?? null,
78
+ args.papatto_conditions ? JSON.stringify(args.papatto_conditions) : null,
79
+ now,
80
+ ]);
81
+ }
82
+ export async function updateCampaign(args) {
83
+ await ensureSchema();
84
+ const sets = [];
85
+ const params = [];
86
+ let i = 1;
87
+ if (args.status !== undefined) {
88
+ sets.push(`status = $${i++}`);
89
+ params.push(args.status);
90
+ }
91
+ if (args.extracted_count !== undefined) {
92
+ sets.push(`extracted_count = $${i++}`);
93
+ params.push(args.extracted_count);
94
+ }
95
+ if (args.r2_extracted_url !== undefined) {
96
+ sets.push(`r2_extracted_url = $${i++}`);
97
+ params.push(args.r2_extracted_url);
98
+ }
99
+ if (args.sheet_url !== undefined) {
100
+ sets.push(`sheet_url = $${i++}`);
101
+ params.push(args.sheet_url);
102
+ }
103
+ if (sets.length === 0)
104
+ return;
105
+ sets.push(`updated_at = $${i++}`);
106
+ params.push(Date.now());
107
+ params.push(args.campaign_id);
108
+ await pool().query(`UPDATE papatto.campaigns SET ${sets.join(", ")} WHERE campaign_id = $${i}`, params);
109
+ }
110
+ export async function getCampaign(campaign_id) {
111
+ await ensureSchema();
112
+ const res = await pool().query(`SELECT campaign_id, owner_slack_user_id, natural_language_query,
113
+ papatto_conditions, extracted_count, status,
114
+ created_at, updated_at, r2_extracted_url, sheet_url
115
+ FROM papatto.campaigns
116
+ WHERE campaign_id = $1`, [campaign_id]);
117
+ return res.rows[0] ?? null;
118
+ }
119
+ export async function listCampaignsByOwner(owner_slack_user_id, limit = 20) {
120
+ await ensureSchema();
121
+ const res = await pool().query(`SELECT campaign_id, owner_slack_user_id, natural_language_query,
122
+ papatto_conditions, extracted_count, status,
123
+ created_at, updated_at, r2_extracted_url, sheet_url
124
+ FROM papatto.campaigns
125
+ WHERE owner_slack_user_id = $1
126
+ ORDER BY created_at DESC
127
+ LIMIT $2`, [owner_slack_user_id, limit]);
128
+ return res.rows;
129
+ }
130
+ export async function closePool() {
131
+ if (cachedPool) {
132
+ await cachedPool.end();
133
+ cachedPool = null;
134
+ schemaReady = false;
135
+ }
136
+ }
@@ -0,0 +1,25 @@
1
+ export interface R2Env {
2
+ accessKeyId: string;
3
+ secretAccessKey: string;
4
+ endpoint: string;
5
+ bucket: string;
6
+ }
7
+ export declare function isConfigured(): boolean;
8
+ export declare function r2Url(key: string): string | null;
9
+ export declare function uploadFile(key: string, localPath: string, contentType?: string): Promise<{
10
+ key: string;
11
+ size: number;
12
+ url: string;
13
+ }>;
14
+ export declare function uploadBuffer(key: string, body: Buffer, contentType: string): Promise<{
15
+ key: string;
16
+ url: string;
17
+ }>;
18
+ export declare function uploadJson(key: string, data: unknown): Promise<{
19
+ key: string;
20
+ url: string;
21
+ }>;
22
+ export declare function csvKey(campaignId: string, suggestedName: string, ts?: number): string;
23
+ export declare function auditKey(slackUserId: string, label: string, ts?: number): string;
24
+ export declare function errorKey(label: string, ts?: number): string;
25
+ export declare function errorImageKey(label: string, ts?: number): string;
@@ -0,0 +1,97 @@
1
+ import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
2
+ import { createReadStream } from "node:fs";
3
+ import { statSync } from "node:fs";
4
+ import { basename } from "node:path";
5
+ import { retry } from "../shared/retry.js";
6
+ import { log } from "../shared/logger.js";
7
+ let cachedClient = null;
8
+ function readEnv() {
9
+ const accessKeyId = process.env.R2_ACCESS_KEY_ID;
10
+ const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY;
11
+ const endpoint = process.env.R2_ENDPOINT;
12
+ const bucket = process.env.R2_BUCKET;
13
+ if (!accessKeyId || !secretAccessKey || !endpoint || !bucket)
14
+ return null;
15
+ return { accessKeyId, secretAccessKey, endpoint, bucket };
16
+ }
17
+ export function isConfigured() {
18
+ return readEnv() !== null;
19
+ }
20
+ function client() {
21
+ const env = readEnv();
22
+ if (!env)
23
+ throw new Error("R2 環境変数が未設定です (R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_ENDPOINT, R2_BUCKET)");
24
+ if (!cachedClient) {
25
+ cachedClient = new S3Client({
26
+ region: "auto",
27
+ endpoint: env.endpoint,
28
+ credentials: {
29
+ accessKeyId: env.accessKeyId,
30
+ secretAccessKey: env.secretAccessKey,
31
+ },
32
+ forcePathStyle: true,
33
+ });
34
+ }
35
+ return { s3: cachedClient, env };
36
+ }
37
+ export function r2Url(key) {
38
+ const env = readEnv();
39
+ if (!env)
40
+ return null;
41
+ return `${env.endpoint}/${env.bucket}/${key}`;
42
+ }
43
+ export async function uploadFile(key, localPath, contentType = "application/octet-stream") {
44
+ const { s3, env } = client();
45
+ const size = statSync(localPath).size;
46
+ await retry(() => s3.send(new PutObjectCommand({
47
+ Bucket: env.bucket,
48
+ Key: key,
49
+ Body: createReadStream(localPath),
50
+ ContentType: contentType,
51
+ ContentLength: size,
52
+ })), { attempts: 3, label: "r2.uploadFile" });
53
+ const url = `${env.endpoint}/${env.bucket}/${key}`;
54
+ log("info", "r2 upload ok", { key, size, file: basename(localPath) });
55
+ return { key, size, url };
56
+ }
57
+ export async function uploadBuffer(key, body, contentType) {
58
+ const { s3, env } = client();
59
+ await retry(() => s3.send(new PutObjectCommand({
60
+ Bucket: env.bucket,
61
+ Key: key,
62
+ Body: body,
63
+ ContentType: contentType,
64
+ })), { attempts: 3, label: "r2.uploadBuffer" });
65
+ const url = `${env.endpoint}/${env.bucket}/${key}`;
66
+ log("info", "r2 buffer upload ok", { key, size: body.length, contentType });
67
+ return { key, url };
68
+ }
69
+ export async function uploadJson(key, data) {
70
+ const { s3, env } = client();
71
+ const body = Buffer.from(JSON.stringify(data, null, 2), "utf-8");
72
+ await retry(() => s3.send(new PutObjectCommand({
73
+ Bucket: env.bucket,
74
+ Key: key,
75
+ Body: body,
76
+ ContentType: "application/json; charset=utf-8",
77
+ })), { attempts: 3, label: "r2.uploadJson" });
78
+ const url = `${env.endpoint}/${env.bucket}/${key}`;
79
+ log("info", "r2 json upload ok", { key, size: body.length });
80
+ return { key, url };
81
+ }
82
+ export function csvKey(campaignId, suggestedName, ts = Date.now()) {
83
+ const safe = suggestedName.replace(/[^a-zA-Z0-9._-]/g, "_");
84
+ return `campaigns/${campaignId}/${ts}__${safe}`;
85
+ }
86
+ export function auditKey(slackUserId, label, ts = Date.now()) {
87
+ const date = new Date(ts).toISOString().slice(0, 10);
88
+ return `audit/${date}/${slackUserId}__${label}__${ts}.json`;
89
+ }
90
+ export function errorKey(label, ts = Date.now()) {
91
+ const date = new Date(ts).toISOString().slice(0, 10);
92
+ return `errors/${date}/${label}__${ts}.json`;
93
+ }
94
+ export function errorImageKey(label, ts = Date.now()) {
95
+ const date = new Date(ts).toISOString().slice(0, 10);
96
+ return `errors/${date}/${label}__${ts}.png`;
97
+ }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,6 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ import "./shared/env.js";
3
+ import { BizGateClient } from "./bizgate-client.js";
4
+ import { UsageTracker } from "./usage-tracker.js";
5
+ export declare const usageTracker: UsageTracker;
6
+ export declare const client: BizGateClient;
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ // ---------- .env 自動ロード (must be first) ----------
3
+ import "./shared/env.js";
2
4
  // ---------- --install-skill サブコマンド ----------
3
5
  if (process.argv.includes("--install-skill")) {
4
6
  const { main: installSkill } = await import("./install-skill.js");
@@ -14,6 +16,8 @@ import { UsageTracker } from "./usage-tracker.js";
14
16
  import { SeedCache } from "./seed-cache.js";
15
17
  import { BizGateResultCache } from "./bizgate-cache.js";
16
18
  import { JpxCache } from "./jpx-cache.js";
19
+ import { saveProspectList, searchProspectHistory, getHistoryDir, } from "./prospect-history.js";
20
+ import { registerPapattoTools } from "./papatto/tools.js";
17
21
  import { homedir } from "node:os";
18
22
  import { join, dirname } from "node:path";
19
23
  import { readFileSync } from "node:fs";
@@ -60,8 +64,8 @@ const config = {
60
64
  app: process.env.BIZGATE_APP,
61
65
  dailyLimit,
62
66
  };
63
- const usageTracker = new UsageTracker(usageFile, dailyLimit);
64
- const client = new BizGateClient(config, usageTracker);
67
+ export const usageTracker = new UsageTracker(usageFile, dailyLimit);
68
+ export const client = new BizGateClient(config, usageTracker);
65
69
  const seedCache = new SeedCache(seedCsvUrl);
66
70
  const resultCache = new BizGateResultCache(sharedCacheUrl);
67
71
  const jpxCache = new JpxCache(process.env.JPX_DATA_URL);
@@ -1022,6 +1026,101 @@ server.tool("bizgate__prospect_suggest", "BizGate APIを使わずにローカル
1022
1026
  }],
1023
1027
  };
1024
1028
  });
1029
+ // ---------- Tool 10: プロスペクトリスト保存 ----------
1030
+ server.tool("bizgate__prospect_save", "作成したプロスペクトリストをローカル(~/.bizgate-mcp/history/)にJSONで保存する。保存期間は1ヶ月。後で bizgate__prospect_history で日付・axis・industry で検索できる。prospect-find スキルの最終ステップで呼び出す。", {
1031
+ axis: z.string().optional().describe("提案軸(例: DOMO / 営業代行 / Solution / AI)"),
1032
+ industry: z.string().optional().describe("業種(例: 情報・通信業, 製造業)"),
1033
+ title: z.string().optional().describe("リストのタイトル(例: DOMO × プライム IT企業)"),
1034
+ criteria: z.record(z.unknown()).optional().describe("追加の検索条件(market, region, size など任意)"),
1035
+ companies: z.array(z.object({
1036
+ name: z.string(),
1037
+ corporate_number: z.string().optional(),
1038
+ industry: z.string().optional(),
1039
+ selection_reason: z.string().optional(),
1040
+ departments: z.array(z.object({
1041
+ name: z.string(),
1042
+ tel: z.string().optional(),
1043
+ reason: z.string().optional(),
1044
+ })).optional(),
1045
+ }).passthrough()).describe("企業リスト。最低限 name は必須"),
1046
+ markdown: z.string().optional().describe("結果出力のマークダウン全文(任意)"),
1047
+ }, async ({ axis, industry, title, criteria, companies, markdown }) => {
1048
+ if (!companies || companies.length === 0) {
1049
+ return {
1050
+ content: [{ type: "text", text: "エラー: companies が空です。保存しません。" }],
1051
+ };
1052
+ }
1053
+ const result = saveProspectList({
1054
+ axis,
1055
+ industry,
1056
+ title,
1057
+ criteria,
1058
+ companies: companies,
1059
+ markdown,
1060
+ });
1061
+ return {
1062
+ content: [{
1063
+ type: "text",
1064
+ text: `✅ プロスペクトリストを保存しました。\n\n- ファイル: \`${result.filename}\`\n- 保存先: \`${getHistoryDir()}\`\n- 作成日時: ${result.created_at}\n- 有効期限: ${result.expires_at}(1ヶ月)\n- 件数: ${companies.length}社`,
1065
+ }],
1066
+ };
1067
+ });
1068
+ // ---------- Tool 11: プロスペクト履歴検索 ----------
1069
+ server.tool("bizgate__prospect_history", "過去に保存したプロスペクトリストを検索する。日付(特定日 or 期間)、axis(提案軸)、industry(業種)で絞り込み可能。1ヶ月以上前のリストは自動削除される。", {
1070
+ date: z.string().optional().describe("特定の日付(YYYY-MM-DD)。指定するとその日に作成されたリストのみ"),
1071
+ date_from: z.string().optional().describe("開始日(YYYY-MM-DD、含む)。期間検索"),
1072
+ date_to: z.string().optional().describe("終了日(YYYY-MM-DD、含む)。期間検索"),
1073
+ axis: z.string().optional().describe("提案軸キーワード(部分一致、大文字小文字無視)"),
1074
+ industry: z.string().optional().describe("業種キーワード(部分一致、大文字小文字無視)"),
1075
+ limit: z.number().optional().describe("最大返却件数(デフォルト50、最大200)"),
1076
+ include_markdown: z.boolean().optional().describe("マークダウン全文を含めるか(デフォルト false、件数ヒットしてから詳細閲覧時に true)"),
1077
+ include_companies: z.boolean().optional().describe("企業リスト詳細を含めるか(デフォルト false)"),
1078
+ }, async ({ date, date_from, date_to, axis, industry, limit, include_markdown, include_companies }) => {
1079
+ const hits = searchProspectHistory({
1080
+ date,
1081
+ date_from,
1082
+ date_to,
1083
+ axis,
1084
+ industry,
1085
+ limit,
1086
+ include_markdown,
1087
+ include_companies,
1088
+ });
1089
+ if (hits.length === 0) {
1090
+ return {
1091
+ content: [{ type: "text", text: "該当するプロスペクトリストは見つかりませんでした。" }],
1092
+ };
1093
+ }
1094
+ const header = `## 過去のプロスペクトリスト(${hits.length}件)\n\n`;
1095
+ const table = `| # | 作成日時 | タイトル | 提案軸 | 業種 | 件数 | ファイル |\n|---|---------|---------|-------|------|------|--------|\n` +
1096
+ hits
1097
+ .map((h, i) => {
1098
+ const dt = h.created_at.replace("T", " ").slice(0, 16);
1099
+ const title = h.title ?? "-";
1100
+ const ax = h.axis ?? "-";
1101
+ const ind = h.industry ?? "-";
1102
+ return `| ${i + 1} | ${dt} | ${title} | ${ax} | ${ind} | ${h.company_count} | \`${h.filename}\` |`;
1103
+ })
1104
+ .join("\n");
1105
+ let detail = "";
1106
+ if (include_companies || include_markdown) {
1107
+ detail = "\n\n---\n\n" + hits.map((h, i) => {
1108
+ const parts = [`### ${i + 1}. ${h.title ?? h.filename}`];
1109
+ if (include_markdown && h.markdown) {
1110
+ parts.push("\n" + h.markdown);
1111
+ }
1112
+ else if (include_companies && h.companies) {
1113
+ parts.push("\n" + h.companies.map((c, ci) => `${ci + 1}. ${c.name}${c.industry ? `(${c.industry})` : ""}`).join("\n"));
1114
+ }
1115
+ return parts.join("\n");
1116
+ }).join("\n\n");
1117
+ }
1118
+ return {
1119
+ content: [{ type: "text", text: header + table + detail }],
1120
+ };
1121
+ });
1122
+ // ---------- Papatto ツール登録 ----------
1123
+ registerPapattoTools(server);
1025
1124
  // ---------- 起動 ----------
1026
1125
  async function main() {
1027
1126
  // シードキャッシュ + 共有BizGateキャッシュを非同期で初期化
@@ -0,0 +1,12 @@
1
+ export interface PapattoCredential {
2
+ papatto_email: string;
3
+ papatto_password: string;
4
+ }
5
+ export interface StoredAccount {
6
+ slack_user_id: string;
7
+ papatto_email: string;
8
+ }
9
+ export declare function setCredential(slackUserId: string, email: string, password: string): Promise<void>;
10
+ export declare function getCredential(slackUserId: string): Promise<PapattoCredential | null>;
11
+ export declare function deleteCredential(slackUserId: string): Promise<boolean>;
12
+ export declare function listAccounts(): Promise<StoredAccount[]>;
@@ -0,0 +1,40 @@
1
+ import keytar from "keytar";
2
+ import { log } from "../shared/logger.js";
3
+ const SERVICE_NAME = "bizgate-mcp-papatto";
4
+ function accountKey(slackUserId) {
5
+ return `slack:${slackUserId}`;
6
+ }
7
+ function emailKey(slackUserId) {
8
+ return `email:${slackUserId}`;
9
+ }
10
+ export async function setCredential(slackUserId, email, password) {
11
+ if (!slackUserId || !email || !password) {
12
+ throw new Error("slack_user_id, papatto_email, papatto_password are all required");
13
+ }
14
+ await keytar.setPassword(SERVICE_NAME, accountKey(slackUserId), password);
15
+ await keytar.setPassword(SERVICE_NAME, emailKey(slackUserId), email);
16
+ log("info", "papatto credential saved", { slack_user_id: slackUserId });
17
+ }
18
+ export async function getCredential(slackUserId) {
19
+ const password = await keytar.getPassword(SERVICE_NAME, accountKey(slackUserId));
20
+ const email = await keytar.getPassword(SERVICE_NAME, emailKey(slackUserId));
21
+ if (!password || !email)
22
+ return null;
23
+ return { papatto_email: email, papatto_password: password };
24
+ }
25
+ export async function deleteCredential(slackUserId) {
26
+ const a = await keytar.deletePassword(SERVICE_NAME, accountKey(slackUserId));
27
+ const b = await keytar.deletePassword(SERVICE_NAME, emailKey(slackUserId));
28
+ log("info", "papatto credential deleted", { slack_user_id: slackUserId });
29
+ return a || b;
30
+ }
31
+ export async function listAccounts() {
32
+ const all = await keytar.findCredentials(SERVICE_NAME);
33
+ const map = new Map();
34
+ for (const { account, password } of all) {
35
+ if (account.startsWith("email:")) {
36
+ map.set(account.slice("email:".length), password);
37
+ }
38
+ }
39
+ return Array.from(map, ([slack_user_id, papatto_email]) => ({ slack_user_id, papatto_email }));
40
+ }
@@ -0,0 +1,9 @@
1
+ export interface ParsedCsv {
2
+ headers: string[];
3
+ rows: Record<string, string>[];
4
+ encoding: "utf-8" | "shift_jis";
5
+ total_rows: number;
6
+ }
7
+ export declare function parseCsv(filePath: string): ParsedCsv;
8
+ export declare function sortRowsBy(rows: Record<string, string>[], fieldPatterns: RegExp[], direction?: "desc" | "asc"): Record<string, string>[];
9
+ export declare function takeTopN(rows: Record<string, string>[], n: number): Record<string, string>[];