bizgate-mcp-server 0.4.0 → 0.5.0

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/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" \
@@ -2,35 +2,28 @@ import { type PoolClient } from "pg";
2
2
  export declare function isConfigured(): boolean;
3
3
  export declare function ensureSchema(): Promise<void>;
4
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
5
  export interface CampaignRow {
11
6
  campaign_id: string;
12
7
  owner_slack_user_id: string;
13
8
  natural_language_query: string | null;
14
- papatto_conditions: unknown;
9
+ enrich_input: unknown;
15
10
  extracted_count: number | null;
16
11
  status: "extracting" | "done" | "failed" | string;
17
12
  created_at: number;
18
13
  updated_at: number;
19
14
  r2_extracted_url: string | null;
20
- sheet_url: string | null;
21
15
  }
22
16
  export declare function insertCampaign(args: {
23
17
  campaign_id: string;
24
18
  owner_slack_user_id: string;
25
19
  natural_language_query?: string;
26
- papatto_conditions?: unknown;
20
+ enrich_input?: unknown;
27
21
  }): Promise<void>;
28
22
  export declare function updateCampaign(args: {
29
23
  campaign_id: string;
30
24
  status?: "extracting" | "done" | "failed";
31
25
  extracted_count?: number;
32
26
  r2_extracted_url?: string;
33
- sheet_url?: string;
34
27
  }): Promise<void>;
35
28
  export declare function getCampaign(campaign_id: string): Promise<CampaignRow | null>;
36
29
  export declare function listCampaignsByOwner(owner_slack_user_id: string, limit?: number): Promise<CampaignRow[]>;
@@ -14,7 +14,7 @@ function pool() {
14
14
  if (!cachedPool) {
15
15
  const url = process.env.NEON_DATABASE_URL;
16
16
  if (!url)
17
- throw new Error("NEON_DATABASE_URL 환경변수가 설정되지 않았습니다");
17
+ throw new Error("NEON_DATABASE_URL 環境変数が設定されていません");
18
18
  cachedPool = new Pool({
19
19
  connectionString: url,
20
20
  max: 4,
@@ -51,31 +51,21 @@ export async function withTx(fn) {
51
51
  client.release();
52
52
  }
53
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
54
  export async function insertCampaign(args) {
65
55
  await ensureSchema();
66
56
  const now = Date.now();
67
57
  await pool().query(`INSERT INTO papatto.campaigns
68
- (campaign_id, owner_slack_user_id, natural_language_query, papatto_conditions,
58
+ (campaign_id, owner_slack_user_id, natural_language_query, enrich_input,
69
59
  status, created_at, updated_at)
70
60
  VALUES ($1, $2, $3, $4, 'extracting', $5, $5)
71
61
  ON CONFLICT (campaign_id) DO UPDATE
72
62
  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),
63
+ enrich_input = COALESCE(EXCLUDED.enrich_input, papatto.campaigns.enrich_input),
74
64
  updated_at = EXCLUDED.updated_at`, [
75
65
  args.campaign_id,
76
66
  args.owner_slack_user_id,
77
67
  args.natural_language_query ?? null,
78
- args.papatto_conditions ? JSON.stringify(args.papatto_conditions) : null,
68
+ args.enrich_input ? JSON.stringify(args.enrich_input) : null,
79
69
  now,
80
70
  ]);
81
71
  }
@@ -96,10 +86,6 @@ export async function updateCampaign(args) {
96
86
  sets.push(`r2_extracted_url = $${i++}`);
97
87
  params.push(args.r2_extracted_url);
98
88
  }
99
- if (args.sheet_url !== undefined) {
100
- sets.push(`sheet_url = $${i++}`);
101
- params.push(args.sheet_url);
102
- }
103
89
  if (sets.length === 0)
104
90
  return;
105
91
  sets.push(`updated_at = $${i++}`);
@@ -110,8 +96,8 @@ export async function updateCampaign(args) {
110
96
  export async function getCampaign(campaign_id) {
111
97
  await ensureSchema();
112
98
  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
99
+ enrich_input, extracted_count, status,
100
+ created_at, updated_at, r2_extracted_url
115
101
  FROM papatto.campaigns
116
102
  WHERE campaign_id = $1`, [campaign_id]);
117
103
  return res.rows[0] ?? null;
@@ -119,8 +105,8 @@ export async function getCampaign(campaign_id) {
119
105
  export async function listCampaignsByOwner(owner_slack_user_id, limit = 20) {
120
106
  await ensureSchema();
121
107
  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
108
+ enrich_input, extracted_count, status,
109
+ created_at, updated_at, r2_extracted_url
124
110
  FROM papatto.campaigns
125
111
  WHERE owner_slack_user_id = $1
126
112
  ORDER BY created_at DESC
@@ -0,0 +1,38 @@
1
+ export interface CompanyInput {
2
+ name: string;
3
+ corporate_number?: string;
4
+ }
5
+ export interface EnrichedCompany {
6
+ name: string;
7
+ bizgate_matched: boolean;
8
+ match_pattern?: string;
9
+ corporate_number?: string;
10
+ ceo?: string;
11
+ revenue?: string;
12
+ industry?: string;
13
+ hp_url?: string;
14
+ email?: string;
15
+ capital?: string;
16
+ emp?: string;
17
+ address?: string;
18
+ phone?: string;
19
+ departments?: Array<{
20
+ name: string;
21
+ phone?: string;
22
+ }>;
23
+ keymen?: Array<{
24
+ role: string;
25
+ }>;
26
+ priority_score: number;
27
+ priority_reason?: string;
28
+ }
29
+ export declare function heuristicScore(c: EnrichedCompany): {
30
+ score: number;
31
+ reason: string;
32
+ };
33
+ export declare function enrichWithCompanySearch(rows: CompanyInput[]): Promise<{
34
+ items: EnrichedCompany[];
35
+ api_calls: number;
36
+ }>;
37
+ export declare function enrichDepartmentsAndKeymen(targets: EnrichedCompany[]): Promise<number>;
38
+ export declare function rowsToCsv(items: EnrichedCompany[]): string;
@@ -0,0 +1,171 @@
1
+ import { client as bizgateClient } from "../index.js";
2
+ import { first } from "../bizgate-client.js";
3
+ import { log } from "../shared/logger.js";
4
+ export function heuristicScore(c) {
5
+ let s = 50;
6
+ const reasons = [];
7
+ const cap = c.capital ?? "";
8
+ if (/1兆|5000億|1000億/.test(cap)) {
9
+ s += 25;
10
+ reasons.push("超大企業規模");
11
+ }
12
+ else if (/500億|300億/.test(cap)) {
13
+ s += 18;
14
+ reasons.push("大企業");
15
+ }
16
+ else if (/100億|50億/.test(cap)) {
17
+ s += 10;
18
+ reasons.push("中堅規模");
19
+ }
20
+ const emp = c.emp ?? "";
21
+ if (/5000人|1000人/.test(emp)) {
22
+ s += 12;
23
+ reasons.push("従業員 1000+");
24
+ }
25
+ else if (/500人|100人/.test(emp)) {
26
+ s += 6;
27
+ reasons.push("従業員 100~");
28
+ }
29
+ if (c.bizgate_matched) {
30
+ s += 5;
31
+ reasons.push("BizGate データ確保");
32
+ }
33
+ if (c.departments && c.departments.length >= 3) {
34
+ s += 5;
35
+ reasons.push(`部署 ${c.departments.length} 件`);
36
+ }
37
+ if (c.keymen && c.keymen.length > 0) {
38
+ s += 10;
39
+ reasons.push("キーマン情報あり");
40
+ }
41
+ if (c.hp_url) {
42
+ s += 2;
43
+ }
44
+ if (c.email) {
45
+ s += 3;
46
+ reasons.push("メール公開");
47
+ }
48
+ return {
49
+ score: Math.min(100, s),
50
+ reason: reasons.slice(0, 3).join(" / ") || "基本情報のみ",
51
+ };
52
+ }
53
+ export async function enrichWithCompanySearch(rows) {
54
+ const items = [];
55
+ let calls = 0;
56
+ for (const r of rows) {
57
+ const ec = {
58
+ name: r.name,
59
+ corporate_number: r.corporate_number,
60
+ bizgate_matched: false,
61
+ priority_score: 0,
62
+ };
63
+ try {
64
+ const params = { shogo: r.name };
65
+ if (r.corporate_number)
66
+ params.compno = r.corporate_number;
67
+ const { matchPattern, docs } = await bizgateClient.searchCompany(params);
68
+ calls++;
69
+ if (docs.length > 0) {
70
+ const d = docs[0];
71
+ ec.bizgate_matched = true;
72
+ ec.match_pattern = matchPattern;
73
+ ec.corporate_number = d.compno ?? ec.corporate_number;
74
+ ec.ceo = first(d.ceo);
75
+ ec.revenue = d.revenue ? String(d.revenue) : undefined;
76
+ ec.industry = first(d.gyoshu_facet);
77
+ ec.hp_url = first(d.hpurl);
78
+ ec.email = first(d.mail);
79
+ ec.capital = d.shihon ? String(d.shihon) : undefined;
80
+ ec.emp = d.emp ? String(d.emp) : undefined;
81
+ ec.address = first(d.add);
82
+ ec.phone = first(d.tel);
83
+ }
84
+ }
85
+ catch (e) {
86
+ log("warn", "bizgate company_search failed", { name: r.name, error: String(e) });
87
+ }
88
+ const sc = heuristicScore(ec);
89
+ ec.priority_score = sc.score;
90
+ ec.priority_reason = sc.reason;
91
+ items.push(ec);
92
+ }
93
+ return { items, api_calls: calls };
94
+ }
95
+ export async function enrichDepartmentsAndKeymen(targets) {
96
+ let calls = 0;
97
+ for (const c of targets) {
98
+ if (!c.bizgate_matched)
99
+ continue;
100
+ try {
101
+ const dept = await bizgateClient.searchDepartments({
102
+ compno: c.corporate_number,
103
+ shogo: c.name,
104
+ });
105
+ calls++;
106
+ c.departments = dept.docs.slice(0, 10).map((d) => ({
107
+ name: first(d.bumon) || "(不明)",
108
+ phone: first(d.tel),
109
+ }));
110
+ }
111
+ catch (e) {
112
+ log("warn", "department_search failed", { name: c.name, error: String(e) });
113
+ }
114
+ try {
115
+ const km = await bizgateClient.searchKeyman({
116
+ compno: c.corporate_number,
117
+ shogo: c.name,
118
+ });
119
+ calls++;
120
+ c.keymen = km.docs.slice(0, 5).map((d) => ({ role: first(d.bumon) || "(不明)" }));
121
+ }
122
+ catch (e) {
123
+ log("warn", "keyman_search failed", { name: c.name, error: String(e) });
124
+ }
125
+ const sc = heuristicScore(c);
126
+ c.priority_score = sc.score;
127
+ c.priority_reason = sc.reason;
128
+ }
129
+ return calls;
130
+ }
131
+ function csvEscape(v) {
132
+ if (/[",\r\n]/.test(v))
133
+ return `"${v.replace(/"/g, '""')}"`;
134
+ return v;
135
+ }
136
+ export function rowsToCsv(items) {
137
+ const headers = [
138
+ "priority_score",
139
+ "name",
140
+ "corporate_number",
141
+ "ceo",
142
+ "industry",
143
+ "revenue",
144
+ "capital",
145
+ "emp",
146
+ "address",
147
+ "phone",
148
+ "hp_url",
149
+ "email",
150
+ "departments",
151
+ "keymen",
152
+ "priority_reason",
153
+ ];
154
+ const lines = [headers.join(",")];
155
+ for (const c of items) {
156
+ const row = headers.map((h) => {
157
+ switch (h) {
158
+ case "departments":
159
+ return csvEscape(c.departments?.map((d) => `${d.name}${d.phone ? `(${d.phone})` : ""}`).join("; ") ?? "");
160
+ case "keymen":
161
+ return csvEscape(c.keymen?.map((k) => k.role).join("; ") ?? "");
162
+ default: {
163
+ const v = c[h];
164
+ return csvEscape(v != null ? String(v) : "");
165
+ }
166
+ }
167
+ });
168
+ lines.push(row.join(","));
169
+ }
170
+ return lines.join("\n");
171
+ }
@@ -1,2 +1,2 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- export declare function registerPapattoTools(server: McpServer): void;
2
+ export declare function registerEnrichTools(server: McpServer): void;
@@ -0,0 +1,206 @@
1
+ import { z } from "zod";
2
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { enrichWithCompanySearch, enrichDepartmentsAndKeymen, rowsToCsv, } from "./core.js";
6
+ import * as r2 from "../cloud/r2-client.js";
7
+ import * as neon from "../cloud/neon-client.js";
8
+ import { log } from "../shared/logger.js";
9
+ function ok(text) {
10
+ return { content: [{ type: "text", text }] };
11
+ }
12
+ function fail(text) {
13
+ return { content: [{ type: "text", text: `エラー: ${text}` }] };
14
+ }
15
+ function genCampaignId() {
16
+ const d = new Date();
17
+ const pad = (n) => String(n).padStart(2, "0");
18
+ const dt = `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}`;
19
+ const rand = Math.random().toString(36).slice(2, 6);
20
+ return `enrich_${dt}_${rand}`;
21
+ }
22
+ export function registerEnrichTools(server) {
23
+ server.tool("bizgate__enrich_companies", "会社名 / 法人番号 のリストを受け取り BizGate API で詳細情報を補強する。" +
24
+ "depth=basic は会社基本情報のみ、smart (デフォルト) は優先度上位 N 社のみ部署 + キーマンまで深掘り、" +
25
+ "full は全社の部署 + キーマンを取得。" +
26
+ "結果は priority_score (0–100) 降順で返し、CSV をローカル + R2 に保存し、Neon campaigns に記録する。", {
27
+ companies: z
28
+ .array(z.object({
29
+ name: z.string().describe("会社名"),
30
+ corporate_number: z.string().optional().describe("法人番号 (13桁。マッチング精度向上)"),
31
+ }))
32
+ .min(1)
33
+ .describe("補強対象の会社リスト (最低1社)"),
34
+ depth: z
35
+ .enum(["basic", "smart", "full"])
36
+ .optional()
37
+ .describe("補強深度: basic=会社基本情報のみ / smart=上位 N 社のみ部署+キーマン (デフォルト) / full=全社の部署+キーマン"),
38
+ smart_top_n: z.number().optional().describe("smart 時の深堀対象数 (デフォルト 20)"),
39
+ campaign_id: z.string().optional().describe("キャンペーン ID (未指定時は enrich_YYYYMMDD_HHMM_xxxx 自動生成)"),
40
+ owner_slack_user_id: z.string().optional().describe("オーナー Slack ユーザー ID (Neon 記録用、任意)"),
41
+ natural_language: z.string().optional().describe("元の自然言語クエリ (Neon 記録用、任意)"),
42
+ }, async ({ companies, depth, smart_top_n, campaign_id, owner_slack_user_id, natural_language, }) => {
43
+ const t0 = Date.now();
44
+ const cid = campaign_id ?? genCampaignId();
45
+ const d = depth ?? "smart";
46
+ const topN = Math.max(1, Math.min(smart_top_n ?? 20, companies.length));
47
+ const dbActive = neon.isConfigured();
48
+ if (dbActive) {
49
+ try {
50
+ await neon.insertCampaign({
51
+ campaign_id: cid,
52
+ owner_slack_user_id: owner_slack_user_id ?? "(anonymous)",
53
+ natural_language_query: natural_language,
54
+ enrich_input: { companies },
55
+ });
56
+ }
57
+ catch (e) {
58
+ log("warn", "neon insertCampaign (enrich) failed", { campaign_id: cid, error: String(e) });
59
+ }
60
+ }
61
+ try {
62
+ const { items: enriched, api_calls: searchCalls } = await enrichWithCompanySearch(companies);
63
+ let deepCalls = 0;
64
+ let deeplyEnriched = 0;
65
+ if (d === "smart") {
66
+ const sorted = [...enriched].sort((a, b) => b.priority_score - a.priority_score);
67
+ const top = sorted.slice(0, topN).filter((c) => c.bizgate_matched);
68
+ deepCalls = await enrichDepartmentsAndKeymen(top);
69
+ deeplyEnriched = top.length;
70
+ }
71
+ else if (d === "full") {
72
+ const targets = enriched.filter((c) => c.bizgate_matched);
73
+ deepCalls = await enrichDepartmentsAndKeymen(targets);
74
+ deeplyEnriched = targets.length;
75
+ }
76
+ enriched.sort((a, b) => b.priority_score - a.priority_score);
77
+ const dir = join(homedir(), ".bizgate-mcp", "enrich", cid);
78
+ if (!existsSync(dir))
79
+ mkdirSync(dir, { recursive: true });
80
+ const csvPath = join(dir, `enrich__${Date.now()}.csv`);
81
+ writeFileSync(csvPath, rowsToCsv(enriched), "utf-8");
82
+ let r2Url;
83
+ if (r2.isConfigured()) {
84
+ try {
85
+ const key = r2.csvKey(cid, "enrich.csv");
86
+ const up = await r2.uploadFile(key, csvPath, "text/csv; charset=utf-8");
87
+ r2Url = up.url;
88
+ }
89
+ catch (e) {
90
+ log("warn", "r2 upload (enrich) failed", { campaign_id: cid, error: String(e) });
91
+ }
92
+ }
93
+ if (dbActive) {
94
+ try {
95
+ await neon.updateCampaign({
96
+ campaign_id: cid,
97
+ status: "done",
98
+ extracted_count: enriched.length,
99
+ r2_extracted_url: r2Url,
100
+ });
101
+ }
102
+ catch (e) {
103
+ log("warn", "neon updateCampaign (enrich) failed", { campaign_id: cid, error: String(e) });
104
+ }
105
+ }
106
+ const matched = enriched.filter((c) => c.bizgate_matched).length;
107
+ const top5 = enriched.slice(0, 5);
108
+ const lines = [
109
+ `## エンリッチメント完了 (campaign_id=${cid})`,
110
+ `- 入力: ${companies.length} 社`,
111
+ `- BizGate マッチ: ${matched} 社`,
112
+ `- 部署 / キーマン深掘り: ${deeplyEnriched} 社 (depth=${d})`,
113
+ `- BizGate API 利用: ${searchCalls + deepCalls} 回`,
114
+ `- 所要時間: ${((Date.now() - t0) / 1000).toFixed(1)} 秒`,
115
+ `- CSV (ローカル): ${csvPath}`,
116
+ ];
117
+ if (r2Url)
118
+ lines.push(`- CSV (R2): ${r2Url}`);
119
+ if (top5.length > 0) {
120
+ lines.push("", `## 上位 ${top5.length} 社 (priority_score 降順)`);
121
+ for (let i = 0; i < top5.length; i++) {
122
+ const c = top5[i];
123
+ const meta = [];
124
+ if (c.industry)
125
+ meta.push(`業: ${c.industry}`);
126
+ if (c.revenue)
127
+ meta.push(`売: ${c.revenue}`);
128
+ if (c.emp)
129
+ meta.push(`従: ${c.emp}`);
130
+ if (c.address)
131
+ meta.push(`所: ${c.address}`);
132
+ const deptText = c.departments && c.departments.length > 0
133
+ ? ` / 部署 ${c.departments.length} 件`
134
+ : "";
135
+ const keymanText = c.keymen && c.keymen.length > 0
136
+ ? ` / キーマン ${c.keymen.length} 件`
137
+ : "";
138
+ lines.push(`${i + 1}. **${c.name}** (score=${c.priority_score}) — ${c.priority_reason ?? "-"}`);
139
+ if (meta.length > 0 || deptText || keymanText) {
140
+ lines.push(` ${meta.join(" / ")}${deptText}${keymanText}`);
141
+ }
142
+ }
143
+ }
144
+ return ok(lines.join("\n"));
145
+ }
146
+ catch (e) {
147
+ if (dbActive) {
148
+ await neon.updateCampaign({ campaign_id: cid, status: "failed" }).catch(() => undefined);
149
+ }
150
+ return fail(e instanceof Error ? e.message : String(e));
151
+ }
152
+ });
153
+ server.tool("bizgate__campaign_status", "Neon DB に記録された enrich キャンペーンの状態を照会する。campaign_id 単体または owner_slack_user_id で最近 N 件のリスト。R2 URL も返す。NEON_DATABASE_URL 未設定時は利用不可。", {
154
+ campaign_id: z.string().optional().describe("特定キャンペーン照会"),
155
+ owner_slack_user_id: z.string().optional().describe("オーナー別最近リスト (campaign_id 未指定時)"),
156
+ limit: z.number().optional().describe("リスト取得件数 (デフォルト 20)"),
157
+ }, async ({ campaign_id, owner_slack_user_id, limit }) => {
158
+ if (!neon.isConfigured()) {
159
+ return fail("NEON_DATABASE_URL 未設定。データベース連携が必要です。");
160
+ }
161
+ try {
162
+ if (campaign_id) {
163
+ const c = await neon.getCampaign(campaign_id);
164
+ if (!c)
165
+ return ok(`campaign_id=${campaign_id} は見つかりませんでした。`);
166
+ const lines = [
167
+ `## キャンペーン: ${c.campaign_id}`,
168
+ `- オーナー: ${c.owner_slack_user_id}`,
169
+ `- ステータス: ${c.status}`,
170
+ `- 抽出件数: ${c.extracted_count ?? "-"}`,
171
+ `- 作成: ${new Date(Number(c.created_at)).toISOString()}`,
172
+ `- 更新: ${new Date(Number(c.updated_at)).toISOString()}`,
173
+ `- R2: ${c.r2_extracted_url ?? "-"}`,
174
+ ];
175
+ if (c.enrich_input) {
176
+ lines.push("", "### 入力", "```json", JSON.stringify(c.enrich_input, null, 2), "```");
177
+ }
178
+ if (c.natural_language_query) {
179
+ lines.push("", "### 自然言語クエリ", `> ${c.natural_language_query}`);
180
+ }
181
+ return ok(lines.join("\n"));
182
+ }
183
+ if (!owner_slack_user_id) {
184
+ return fail("campaign_id または owner_slack_user_id のいずれかが必要です");
185
+ }
186
+ const rows = await neon.listCampaignsByOwner(owner_slack_user_id, limit ?? 20);
187
+ if (rows.length === 0)
188
+ return ok(`${owner_slack_user_id} の campaign は見つかりませんでした。`);
189
+ const header = `| campaign_id | status | 抽出件数 | 作成日時 | R2 |\n|-------------|--------|----------|----------|----|`;
190
+ const lines = rows.map((c) => {
191
+ const dt = new Date(Number(c.created_at)).toISOString().replace("T", " ").slice(0, 16);
192
+ const r2Short = c.r2_extracted_url ? "✓" : "-";
193
+ return `| ${c.campaign_id} | ${c.status} | ${c.extracted_count ?? "-"} | ${dt} | ${r2Short} |`;
194
+ });
195
+ return ok([
196
+ `## ${owner_slack_user_id} のキャンペーン (最近 ${rows.length} 件)`,
197
+ "",
198
+ header,
199
+ ...lines,
200
+ ].join("\n"));
201
+ }
202
+ catch (e) {
203
+ return fail(e instanceof Error ? e.message : String(e));
204
+ }
205
+ });
206
+ }
@@ -0,0 +1,9 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export interface HttpServerOptions {
3
+ /** Factory function: 한 client connection 마다 새 McpServer 인스턴스 생성. */
4
+ createMcpServer: () => McpServer;
5
+ port?: number;
6
+ host?: string;
7
+ authToken?: string;
8
+ }
9
+ export declare function startHttpServer(opts: HttpServerOptions): Promise<void>;