@line-harness/mcp-server 0.4.1 → 0.6.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/dist/index.js CHANGED
@@ -29,6 +29,84 @@ function getClient() {
29
29
  return clientInstance;
30
30
  }
31
31
 
32
+ // src/tools/auto-track-urls.ts
33
+ async function autoTrackUrls(client, messageContent, messageType, title) {
34
+ if (messageType !== "flex") {
35
+ return { content: messageContent, trackedUrls: [] };
36
+ }
37
+ let parsed;
38
+ try {
39
+ parsed = JSON.parse(messageContent);
40
+ } catch {
41
+ return { content: messageContent, trackedUrls: [] };
42
+ }
43
+ const urlMap = /* @__PURE__ */ new Map();
44
+ collectUris(parsed, urlMap);
45
+ if (urlMap.size === 0) {
46
+ return { content: messageContent, trackedUrls: [] };
47
+ }
48
+ const trackedUrls = [];
49
+ for (const originalUrl of urlMap.keys()) {
50
+ try {
51
+ const link = await client.trackedLinks.create({
52
+ name: `${title} \u2014 ${truncate(originalUrl, 50)}`,
53
+ originalUrl
54
+ });
55
+ urlMap.set(originalUrl, link.trackingUrl);
56
+ trackedUrls.push({ original: originalUrl, tracking: link.trackingUrl });
57
+ } catch {
58
+ }
59
+ }
60
+ replaceUris(parsed, urlMap);
61
+ return {
62
+ content: JSON.stringify(parsed),
63
+ trackedUrls
64
+ };
65
+ }
66
+ function collectUris(obj, urlMap) {
67
+ if (obj === null || obj === void 0 || typeof obj !== "object") return;
68
+ if (Array.isArray(obj)) {
69
+ for (const item of obj) {
70
+ collectUris(item, urlMap);
71
+ }
72
+ return;
73
+ }
74
+ const record = obj;
75
+ if (record.type === "uri" && typeof record.uri === "string") {
76
+ const uri = record.uri;
77
+ if (uri.startsWith("http://") || uri.startsWith("https://")) {
78
+ if (!urlMap.has(uri)) {
79
+ urlMap.set(uri, uri);
80
+ }
81
+ }
82
+ }
83
+ for (const value of Object.values(record)) {
84
+ collectUris(value, urlMap);
85
+ }
86
+ }
87
+ function replaceUris(obj, urlMap) {
88
+ if (obj === null || obj === void 0 || typeof obj !== "object") return;
89
+ if (Array.isArray(obj)) {
90
+ for (const item of obj) {
91
+ replaceUris(item, urlMap);
92
+ }
93
+ return;
94
+ }
95
+ const record = obj;
96
+ if (record.type === "uri" && typeof record.uri === "string") {
97
+ const tracked = urlMap.get(record.uri);
98
+ if (tracked && tracked !== record.uri) {
99
+ record.uri = tracked;
100
+ }
101
+ }
102
+ for (const value of Object.values(record)) {
103
+ replaceUris(value, urlMap);
104
+ }
105
+ }
106
+ function truncate(str, max) {
107
+ return str.length > max ? str.slice(0, max) + "\u2026" : str;
108
+ }
109
+
32
110
  // src/tools/send-message.ts
33
111
  function registerSendMessage(server2) {
34
112
  server2.tool(
@@ -46,9 +124,15 @@ function registerSendMessage(server2) {
46
124
  async ({ friendId, content, messageType }) => {
47
125
  try {
48
126
  const client = getClient();
127
+ const { content: trackedContent } = await autoTrackUrls(
128
+ client,
129
+ content,
130
+ messageType,
131
+ `DM to ${friendId.slice(0, 8)}`
132
+ );
49
133
  const result = await client.friends.sendMessage(
50
134
  friendId,
51
- content,
135
+ trackedContent,
52
136
  messageType
53
137
  );
54
138
  return {
@@ -174,10 +258,16 @@ function registerBroadcast(server2) {
174
258
  isError: true
175
259
  };
176
260
  }
261
+ const { content: trackedContent2 } = await autoTrackUrls(
262
+ client,
263
+ messageContent,
264
+ messageType,
265
+ title
266
+ );
177
267
  const broadcast2 = await client.broadcasts.create({
178
268
  title: `[SEGMENT] ${title}`,
179
269
  messageType,
180
- messageContent,
270
+ messageContent: trackedContent2,
181
271
  targetType: "all",
182
272
  lineAccountId: accountId
183
273
  });
@@ -204,10 +294,16 @@ function registerBroadcast(server2) {
204
294
  throw sendError;
205
295
  }
206
296
  }
297
+ const { content: trackedContent, trackedUrls } = await autoTrackUrls(
298
+ client,
299
+ messageContent,
300
+ messageType,
301
+ title
302
+ );
207
303
  const broadcast = await client.broadcasts.create({
208
304
  title,
209
305
  messageType,
210
- messageContent,
306
+ messageContent: trackedContent,
211
307
  targetType,
212
308
  targetTagId,
213
309
  scheduledAt,
@@ -1054,6 +1150,287 @@ function registerListCrmObjects(server2) {
1054
1150
  );
1055
1151
  }
1056
1152
 
1153
+ // src/tools/manage-ad-platforms.ts
1154
+ import { z as z15 } from "zod";
1155
+ function registerManageAdPlatforms(server2) {
1156
+ server2.tool(
1157
+ "manage_ad_platforms",
1158
+ "Manage ad platform integrations for conversion tracking. Supports Meta (Facebook/Instagram), X (Twitter), Google Ads, and TikTok. Use 'list' to see configured platforms, 'create' to add a new one, 'update' to modify settings, 'delete' to remove, or 'test' to verify the connection.",
1159
+ {
1160
+ action: z15.enum(["list", "create", "update", "delete", "test"]).describe("Action to perform"),
1161
+ platformId: z15.string().optional().describe("Platform ID (required for 'update' and 'delete')"),
1162
+ name: z15.enum(["meta", "x", "google", "tiktok"]).optional().describe("Platform name (required for 'create' and 'test')"),
1163
+ displayName: z15.string().optional().describe("Display name for the platform (e.g. 'Meta\u5E83\u544A')"),
1164
+ config: z15.record(z15.unknown()).optional().describe(
1165
+ "Platform config JSON. Meta: {pixel_id, access_token, test_event_code?}. X: {pixel_id, api_key, api_secret}. Google: {customer_id, conversion_action_id, oauth_token}. TikTok: {pixel_code, access_token}"
1166
+ ),
1167
+ isActive: z15.boolean().optional().describe("Enable/disable the platform (for 'update')"),
1168
+ eventName: z15.string().optional().describe("Event name for test conversion (for 'test', e.g. 'Lead')"),
1169
+ friendId: z15.string().optional().describe("Friend ID for test conversion (for 'test')")
1170
+ },
1171
+ async ({ action, platformId, name, displayName, config, isActive, eventName, friendId }) => {
1172
+ try {
1173
+ const client = getClient();
1174
+ switch (action) {
1175
+ case "list": {
1176
+ const platforms = await client.adPlatforms.list();
1177
+ return {
1178
+ content: [
1179
+ {
1180
+ type: "text",
1181
+ text: JSON.stringify(
1182
+ {
1183
+ success: true,
1184
+ count: platforms.length,
1185
+ platforms
1186
+ },
1187
+ null,
1188
+ 2
1189
+ )
1190
+ }
1191
+ ]
1192
+ };
1193
+ }
1194
+ case "create": {
1195
+ if (!name) throw new Error("name is required for create action");
1196
+ if (!config)
1197
+ throw new Error("config is required for create action");
1198
+ const platform = await client.adPlatforms.create({
1199
+ name,
1200
+ displayName,
1201
+ config
1202
+ });
1203
+ return {
1204
+ content: [
1205
+ {
1206
+ type: "text",
1207
+ text: JSON.stringify({ success: true, platform }, null, 2)
1208
+ }
1209
+ ]
1210
+ };
1211
+ }
1212
+ case "update": {
1213
+ if (!platformId)
1214
+ throw new Error("platformId is required for update action");
1215
+ const platform = await client.adPlatforms.update(platformId, {
1216
+ name,
1217
+ displayName,
1218
+ config,
1219
+ isActive
1220
+ });
1221
+ return {
1222
+ content: [
1223
+ {
1224
+ type: "text",
1225
+ text: JSON.stringify({ success: true, platform }, null, 2)
1226
+ }
1227
+ ]
1228
+ };
1229
+ }
1230
+ case "delete": {
1231
+ if (!platformId)
1232
+ throw new Error("platformId is required for delete action");
1233
+ await client.adPlatforms.delete(platformId);
1234
+ return {
1235
+ content: [
1236
+ {
1237
+ type: "text",
1238
+ text: JSON.stringify({
1239
+ success: true,
1240
+ message: `Platform ${platformId} deleted`
1241
+ })
1242
+ }
1243
+ ]
1244
+ };
1245
+ }
1246
+ case "test": {
1247
+ if (!name) throw new Error("name is required for test action");
1248
+ if (!eventName)
1249
+ throw new Error("eventName is required for test action");
1250
+ const result = await client.adPlatforms.test(
1251
+ name,
1252
+ eventName,
1253
+ friendId
1254
+ );
1255
+ return {
1256
+ content: [
1257
+ {
1258
+ type: "text",
1259
+ text: JSON.stringify({ success: true, ...result }, null, 2)
1260
+ }
1261
+ ]
1262
+ };
1263
+ }
1264
+ default:
1265
+ throw new Error(`Unknown action: ${action}`);
1266
+ }
1267
+ } catch (error) {
1268
+ return {
1269
+ content: [
1270
+ {
1271
+ type: "text",
1272
+ text: JSON.stringify(
1273
+ { success: false, error: String(error) },
1274
+ null,
1275
+ 2
1276
+ )
1277
+ }
1278
+ ],
1279
+ isError: true
1280
+ };
1281
+ }
1282
+ }
1283
+ );
1284
+ }
1285
+
1286
+ // src/tools/get-conversion-logs.ts
1287
+ import { z as z16 } from "zod";
1288
+ function registerGetConversionLogs(server2) {
1289
+ server2.tool(
1290
+ "get_conversion_logs",
1291
+ "View ad conversion send logs for a specific platform. Shows the history of conversion events sent to Meta CAPI, X, Google Ads, or TikTok, including status (sent/failed) and error details.",
1292
+ {
1293
+ platformId: z16.string().describe(
1294
+ "Ad platform ID to get logs for. Use manage_ad_platforms with action 'list' first to get the ID."
1295
+ ),
1296
+ limit: z16.number().optional().default(50).describe("Maximum number of logs to return (default: 50)")
1297
+ },
1298
+ async ({ platformId, limit }) => {
1299
+ try {
1300
+ const client = getClient();
1301
+ const logs = await client.adPlatforms.getLogs(platformId, limit);
1302
+ const summary = {
1303
+ total: logs.length,
1304
+ sent: logs.filter((l) => l.status === "sent").length,
1305
+ failed: logs.filter((l) => l.status === "failed").length
1306
+ };
1307
+ return {
1308
+ content: [
1309
+ {
1310
+ type: "text",
1311
+ text: JSON.stringify(
1312
+ { success: true, summary, logs },
1313
+ null,
1314
+ 2
1315
+ )
1316
+ }
1317
+ ]
1318
+ };
1319
+ } catch (error) {
1320
+ return {
1321
+ content: [
1322
+ {
1323
+ type: "text",
1324
+ text: JSON.stringify(
1325
+ { success: false, error: String(error) },
1326
+ null,
1327
+ 2
1328
+ )
1329
+ }
1330
+ ],
1331
+ isError: true
1332
+ };
1333
+ }
1334
+ }
1335
+ );
1336
+ }
1337
+
1338
+ // src/tools/manage-staff.ts
1339
+ import { z as z17 } from "zod";
1340
+ function registerManageStaff(server2) {
1341
+ server2.tool(
1342
+ "manage_staff",
1343
+ "\u30B9\u30BF\u30C3\u30D5\u30A2\u30AB\u30A6\u30F3\u30C8\u306E\u8FFD\u52A0\u30FB\u4E00\u89A7\u30FB\u66F4\u65B0\u30FB\u524A\u9664\u30FBAPI\u30AD\u30FC\u518D\u751F\u6210\u3002\u30AA\u30FC\u30CA\u30FC\u6A29\u9650\u304C\u5FC5\u8981\u3067\u3059\u3002",
1344
+ {
1345
+ action: z17.enum(["create", "list", "get", "update", "delete", "regenerate_key", "me"]).describe("Action to perform"),
1346
+ name: z17.string().optional().describe("Staff name (for 'create' action)"),
1347
+ email: z17.string().nullable().optional().describe("Staff email (optional, null to clear)"),
1348
+ role: z17.enum(["admin", "staff"]).optional().describe("Staff role (for 'create'/'update')"),
1349
+ staffId: z17.string().optional().describe("Staff ID (for 'get','update','delete','regenerate_key')"),
1350
+ isActive: z17.boolean().optional().describe("Activate/deactivate (for 'update')")
1351
+ },
1352
+ async ({ action, name, email, role, staffId, isActive }) => {
1353
+ try {
1354
+ const client = getClient();
1355
+ if (action === "me") {
1356
+ const profile = await client.staff.me();
1357
+ return {
1358
+ content: [{ type: "text", text: JSON.stringify({ success: true, profile }, null, 2) }]
1359
+ };
1360
+ }
1361
+ if (action === "list") {
1362
+ const members = await client.staff.list();
1363
+ return {
1364
+ content: [{ type: "text", text: JSON.stringify({ success: true, members }, null, 2) }]
1365
+ };
1366
+ }
1367
+ if (action === "create") {
1368
+ if (!name) throw new Error("name is required for create action");
1369
+ if (!role) throw new Error("role is required for create action");
1370
+ const member = await client.staff.create({ name, email, role });
1371
+ return {
1372
+ content: [{
1373
+ type: "text",
1374
+ text: JSON.stringify({
1375
+ success: true,
1376
+ member,
1377
+ note: "API\u30AD\u30FC\u306F\u4E00\u5EA6\u3060\u3051\u8868\u793A\u3055\u308C\u307E\u3059\u3002\u5B89\u5168\u306B\u4FDD\u7BA1\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
1378
+ }, null, 2)
1379
+ }]
1380
+ };
1381
+ }
1382
+ if (action === "get") {
1383
+ if (!staffId) throw new Error("staffId is required for get action");
1384
+ const member = await client.staff.get(staffId);
1385
+ return {
1386
+ content: [{ type: "text", text: JSON.stringify({ success: true, member }, null, 2) }]
1387
+ };
1388
+ }
1389
+ if (action === "update") {
1390
+ if (!staffId) throw new Error("staffId is required for update action");
1391
+ const updates = {};
1392
+ if (name !== void 0) updates.name = name;
1393
+ if (email !== void 0) updates.email = email;
1394
+ if (role !== void 0) updates.role = role;
1395
+ if (isActive !== void 0) updates.isActive = isActive;
1396
+ const member = await client.staff.update(staffId, updates);
1397
+ return {
1398
+ content: [{ type: "text", text: JSON.stringify({ success: true, member }, null, 2) }]
1399
+ };
1400
+ }
1401
+ if (action === "delete") {
1402
+ if (!staffId) throw new Error("staffId is required for delete action");
1403
+ await client.staff.delete(staffId);
1404
+ return {
1405
+ content: [{ type: "text", text: JSON.stringify({ success: true, deleted: staffId }, null, 2) }]
1406
+ };
1407
+ }
1408
+ if (action === "regenerate_key") {
1409
+ if (!staffId) throw new Error("staffId is required for regenerate_key action");
1410
+ const result = await client.staff.regenerateKey(staffId);
1411
+ return {
1412
+ content: [{
1413
+ type: "text",
1414
+ text: JSON.stringify({
1415
+ success: true,
1416
+ staffId,
1417
+ newApiKey: result.apiKey,
1418
+ note: "\u65B0\u3057\u3044API\u30AD\u30FC\u306F\u4E00\u5EA6\u3060\u3051\u8868\u793A\u3055\u308C\u307E\u3059\u3002\u5B89\u5168\u306B\u4FDD\u7BA1\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
1419
+ }, null, 2)
1420
+ }]
1421
+ };
1422
+ }
1423
+ throw new Error(`Unknown action: ${action}`);
1424
+ } catch (error) {
1425
+ return {
1426
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }, null, 2) }],
1427
+ isError: true
1428
+ };
1429
+ }
1430
+ }
1431
+ );
1432
+ }
1433
+
1057
1434
  // src/tools/index.ts
1058
1435
  function registerAllTools(server2) {
1059
1436
  registerSendMessage(server2);
@@ -1070,6 +1447,9 @@ function registerAllTools(server2) {
1070
1447
  registerGetLinkClicks(server2);
1071
1448
  registerAccountSummary(server2);
1072
1449
  registerListCrmObjects(server2);
1450
+ registerManageAdPlatforms(server2);
1451
+ registerGetConversionLogs(server2);
1452
+ registerManageStaff(server2);
1073
1453
  }
1074
1454
 
1075
1455
  // src/resources/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@line-harness/mcp-server",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "line-harness-mcp": "./dist/index.js"
@@ -0,0 +1,112 @@
1
+ import type { LineHarness } from "@line-harness/sdk";
2
+
3
+ /**
4
+ * Recursively find all URI action URLs in a Flex Message object,
5
+ * create tracked links for each, and replace with tracking URLs.
6
+ */
7
+ export async function autoTrackUrls(
8
+ client: LineHarness,
9
+ messageContent: string,
10
+ messageType: string,
11
+ title: string,
12
+ ): Promise<{ content: string; trackedUrls: { original: string; tracking: string }[] }> {
13
+ if (messageType !== "flex") {
14
+ return { content: messageContent, trackedUrls: [] };
15
+ }
16
+
17
+ let parsed: unknown;
18
+ try {
19
+ parsed = JSON.parse(messageContent);
20
+ } catch {
21
+ return { content: messageContent, trackedUrls: [] };
22
+ }
23
+
24
+ const urlMap = new Map<string, string>();
25
+
26
+ // Collect all unique URIs from the flex message
27
+ collectUris(parsed, urlMap);
28
+
29
+ if (urlMap.size === 0) {
30
+ return { content: messageContent, trackedUrls: [] };
31
+ }
32
+
33
+ // Create tracked links for each unique URL
34
+ const trackedUrls: { original: string; tracking: string }[] = [];
35
+ for (const originalUrl of urlMap.keys()) {
36
+ try {
37
+ const link = await client.trackedLinks.create({
38
+ name: `${title} — ${truncate(originalUrl, 50)}`,
39
+ originalUrl,
40
+ });
41
+ urlMap.set(originalUrl, link.trackingUrl);
42
+ trackedUrls.push({ original: originalUrl, tracking: link.trackingUrl });
43
+ } catch {
44
+ // If tracked link creation fails, keep original URL
45
+ }
46
+ }
47
+
48
+ // Replace URLs in the parsed object
49
+ replaceUris(parsed, urlMap);
50
+
51
+ return {
52
+ content: JSON.stringify(parsed),
53
+ trackedUrls,
54
+ };
55
+ }
56
+
57
+ function collectUris(obj: unknown, urlMap: Map<string, string>): void {
58
+ if (obj === null || obj === undefined || typeof obj !== "object") return;
59
+
60
+ if (Array.isArray(obj)) {
61
+ for (const item of obj) {
62
+ collectUris(item, urlMap);
63
+ }
64
+ return;
65
+ }
66
+
67
+ const record = obj as Record<string, unknown>;
68
+
69
+ // Check if this is a URI action
70
+ if (record.type === "uri" && typeof record.uri === "string") {
71
+ const uri = record.uri;
72
+ // Only track http/https URLs, skip LINE-specific URIs
73
+ if (uri.startsWith("http://") || uri.startsWith("https://")) {
74
+ if (!urlMap.has(uri)) {
75
+ urlMap.set(uri, uri); // placeholder, replaced after creation
76
+ }
77
+ }
78
+ }
79
+
80
+ // Recurse into all values
81
+ for (const value of Object.values(record)) {
82
+ collectUris(value, urlMap);
83
+ }
84
+ }
85
+
86
+ function replaceUris(obj: unknown, urlMap: Map<string, string>): void {
87
+ if (obj === null || obj === undefined || typeof obj !== "object") return;
88
+
89
+ if (Array.isArray(obj)) {
90
+ for (const item of obj) {
91
+ replaceUris(item, urlMap);
92
+ }
93
+ return;
94
+ }
95
+
96
+ const record = obj as Record<string, unknown>;
97
+
98
+ if (record.type === "uri" && typeof record.uri === "string") {
99
+ const tracked = urlMap.get(record.uri);
100
+ if (tracked && tracked !== record.uri) {
101
+ record.uri = tracked;
102
+ }
103
+ }
104
+
105
+ for (const value of Object.values(record)) {
106
+ replaceUris(value, urlMap);
107
+ }
108
+ }
109
+
110
+ function truncate(str: string, max: number): string {
111
+ return str.length > max ? str.slice(0, max) + "…" : str;
112
+ }
@@ -1,6 +1,7 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import { getClient } from "../client.js";
4
+ import { autoTrackUrls } from "./auto-track-urls.js";
4
5
 
5
6
  export function registerBroadcast(server: McpServer): void {
6
7
  server.tool(
@@ -117,10 +118,17 @@ export function registerBroadcast(server: McpServer): void {
117
118
  };
118
119
  }
119
120
 
121
+ const { content: trackedContent } = await autoTrackUrls(
122
+ client,
123
+ messageContent,
124
+ messageType,
125
+ title,
126
+ );
127
+
120
128
  const broadcast = await client.broadcasts.create({
121
129
  title: `[SEGMENT] ${title}`,
122
130
  messageType,
123
- messageContent,
131
+ messageContent: trackedContent,
124
132
  targetType: "all",
125
133
  lineAccountId: accountId,
126
134
  });
@@ -148,11 +156,19 @@ export function registerBroadcast(server: McpServer): void {
148
156
  }
149
157
  }
150
158
 
159
+ // Auto-track URLs in flex messages
160
+ const { content: trackedContent, trackedUrls } = await autoTrackUrls(
161
+ client,
162
+ messageContent,
163
+ messageType,
164
+ title,
165
+ );
166
+
151
167
  // At this point targetType is guaranteed to be 'all' or 'tag' (segment handled above)
152
168
  const broadcast = await client.broadcasts.create({
153
169
  title,
154
170
  messageType,
155
- messageContent,
171
+ messageContent: trackedContent,
156
172
  targetType: targetType as "all" | "tag",
157
173
  targetTagId,
158
174
  scheduledAt,
@@ -0,0 +1,61 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { getClient } from "../client.js";
4
+
5
+ export function registerGetConversionLogs(server: McpServer): void {
6
+ server.tool(
7
+ "get_conversion_logs",
8
+ "View ad conversion send logs for a specific platform. Shows the history of conversion events sent to Meta CAPI, X, Google Ads, or TikTok, including status (sent/failed) and error details.",
9
+ {
10
+ platformId: z
11
+ .string()
12
+ .describe(
13
+ "Ad platform ID to get logs for. Use manage_ad_platforms with action 'list' first to get the ID.",
14
+ ),
15
+ limit: z
16
+ .number()
17
+ .optional()
18
+ .default(50)
19
+ .describe("Maximum number of logs to return (default: 50)"),
20
+ },
21
+ async ({ platformId, limit }) => {
22
+ try {
23
+ const client = getClient();
24
+ const logs = await client.adPlatforms.getLogs(platformId, limit);
25
+
26
+ const summary = {
27
+ total: logs.length,
28
+ sent: logs.filter((l) => l.status === "sent").length,
29
+ failed: logs.filter((l) => l.status === "failed").length,
30
+ };
31
+
32
+ return {
33
+ content: [
34
+ {
35
+ type: "text" as const,
36
+ text: JSON.stringify(
37
+ { success: true, summary, logs },
38
+ null,
39
+ 2,
40
+ ),
41
+ },
42
+ ],
43
+ };
44
+ } catch (error) {
45
+ return {
46
+ content: [
47
+ {
48
+ type: "text" as const,
49
+ text: JSON.stringify(
50
+ { success: false, error: String(error) },
51
+ null,
52
+ 2,
53
+ ),
54
+ },
55
+ ],
56
+ isError: true,
57
+ };
58
+ }
59
+ },
60
+ );
61
+ }
@@ -13,6 +13,9 @@ import { registerGetFormSubmissions } from "./get-form-submissions.js";
13
13
  import { registerGetLinkClicks } from "./get-link-clicks.js";
14
14
  import { registerAccountSummary } from "./account-summary.js";
15
15
  import { registerListCrmObjects } from "./list-crm-objects.js";
16
+ import { registerManageAdPlatforms } from "./manage-ad-platforms.js";
17
+ import { registerGetConversionLogs } from "./get-conversion-logs.js";
18
+ import { registerManageStaff } from "./manage-staff.js";
16
19
 
17
20
  export function registerAllTools(server: McpServer): void {
18
21
  registerSendMessage(server);
@@ -29,4 +32,7 @@ export function registerAllTools(server: McpServer): void {
29
32
  registerGetLinkClicks(server);
30
33
  registerAccountSummary(server);
31
34
  registerListCrmObjects(server);
35
+ registerManageAdPlatforms(server);
36
+ registerGetConversionLogs(server);
37
+ registerManageStaff(server);
32
38
  }
@@ -0,0 +1,170 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { getClient } from "../client.js";
4
+
5
+ export function registerManageAdPlatforms(server: McpServer): void {
6
+ server.tool(
7
+ "manage_ad_platforms",
8
+ "Manage ad platform integrations for conversion tracking. Supports Meta (Facebook/Instagram), X (Twitter), Google Ads, and TikTok. Use 'list' to see configured platforms, 'create' to add a new one, 'update' to modify settings, 'delete' to remove, or 'test' to verify the connection.",
9
+ {
10
+ action: z
11
+ .enum(["list", "create", "update", "delete", "test"])
12
+ .describe("Action to perform"),
13
+ platformId: z
14
+ .string()
15
+ .optional()
16
+ .describe("Platform ID (required for 'update' and 'delete')"),
17
+ name: z
18
+ .enum(["meta", "x", "google", "tiktok"])
19
+ .optional()
20
+ .describe("Platform name (required for 'create' and 'test')"),
21
+ displayName: z
22
+ .string()
23
+ .optional()
24
+ .describe("Display name for the platform (e.g. 'Meta広告')"),
25
+ config: z
26
+ .record(z.unknown())
27
+ .optional()
28
+ .describe(
29
+ "Platform config JSON. Meta: {pixel_id, access_token, test_event_code?}. X: {pixel_id, api_key, api_secret}. Google: {customer_id, conversion_action_id, oauth_token}. TikTok: {pixel_code, access_token}",
30
+ ),
31
+ isActive: z
32
+ .boolean()
33
+ .optional()
34
+ .describe("Enable/disable the platform (for 'update')"),
35
+ eventName: z
36
+ .string()
37
+ .optional()
38
+ .describe("Event name for test conversion (for 'test', e.g. 'Lead')"),
39
+ friendId: z
40
+ .string()
41
+ .optional()
42
+ .describe("Friend ID for test conversion (for 'test')"),
43
+ },
44
+ async ({ action, platformId, name, displayName, config, isActive, eventName, friendId }) => {
45
+ try {
46
+ const client = getClient();
47
+
48
+ switch (action) {
49
+ case "list": {
50
+ const platforms = await client.adPlatforms.list();
51
+ return {
52
+ content: [
53
+ {
54
+ type: "text" as const,
55
+ text: JSON.stringify(
56
+ {
57
+ success: true,
58
+ count: platforms.length,
59
+ platforms,
60
+ },
61
+ null,
62
+ 2,
63
+ ),
64
+ },
65
+ ],
66
+ };
67
+ }
68
+
69
+ case "create": {
70
+ if (!name) throw new Error("name is required for create action");
71
+ if (!config)
72
+ throw new Error("config is required for create action");
73
+
74
+ const platform = await client.adPlatforms.create({
75
+ name,
76
+ displayName,
77
+ config,
78
+ });
79
+
80
+ return {
81
+ content: [
82
+ {
83
+ type: "text" as const,
84
+ text: JSON.stringify({ success: true, platform }, null, 2),
85
+ },
86
+ ],
87
+ };
88
+ }
89
+
90
+ case "update": {
91
+ if (!platformId)
92
+ throw new Error("platformId is required for update action");
93
+
94
+ const platform = await client.adPlatforms.update(platformId, {
95
+ name,
96
+ displayName,
97
+ config,
98
+ isActive,
99
+ });
100
+
101
+ return {
102
+ content: [
103
+ {
104
+ type: "text" as const,
105
+ text: JSON.stringify({ success: true, platform }, null, 2),
106
+ },
107
+ ],
108
+ };
109
+ }
110
+
111
+ case "delete": {
112
+ if (!platformId)
113
+ throw new Error("platformId is required for delete action");
114
+
115
+ await client.adPlatforms.delete(platformId);
116
+ return {
117
+ content: [
118
+ {
119
+ type: "text" as const,
120
+ text: JSON.stringify({
121
+ success: true,
122
+ message: `Platform ${platformId} deleted`,
123
+ }),
124
+ },
125
+ ],
126
+ };
127
+ }
128
+
129
+ case "test": {
130
+ if (!name) throw new Error("name is required for test action");
131
+ if (!eventName)
132
+ throw new Error("eventName is required for test action");
133
+
134
+ const result = await client.adPlatforms.test(
135
+ name,
136
+ eventName,
137
+ friendId,
138
+ );
139
+
140
+ return {
141
+ content: [
142
+ {
143
+ type: "text" as const,
144
+ text: JSON.stringify({ success: true, ...result }, null, 2),
145
+ },
146
+ ],
147
+ };
148
+ }
149
+
150
+ default:
151
+ throw new Error(`Unknown action: ${action}`);
152
+ }
153
+ } catch (error) {
154
+ return {
155
+ content: [
156
+ {
157
+ type: "text" as const,
158
+ text: JSON.stringify(
159
+ { success: false, error: String(error) },
160
+ null,
161
+ 2,
162
+ ),
163
+ },
164
+ ],
165
+ isError: true,
166
+ };
167
+ }
168
+ },
169
+ );
170
+ }
@@ -0,0 +1,104 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { getClient } from "../client.js";
4
+
5
+ export function registerManageStaff(server: McpServer): void {
6
+ server.tool(
7
+ "manage_staff",
8
+ "スタッフアカウントの追加・一覧・更新・削除・APIキー再生成。オーナー権限が必要です。",
9
+ {
10
+ action: z
11
+ .enum(["create", "list", "get", "update", "delete", "regenerate_key", "me"])
12
+ .describe("Action to perform"),
13
+ name: z.string().optional().describe("Staff name (for 'create' action)"),
14
+ email: z.string().nullable().optional().describe("Staff email (optional, null to clear)"),
15
+ role: z.enum(["admin", "staff"]).optional().describe("Staff role (for 'create'/'update')"),
16
+ staffId: z.string().optional().describe("Staff ID (for 'get','update','delete','regenerate_key')"),
17
+ isActive: z.boolean().optional().describe("Activate/deactivate (for 'update')"),
18
+ },
19
+ async ({ action, name, email, role, staffId, isActive }) => {
20
+ try {
21
+ const client = getClient();
22
+
23
+ if (action === "me") {
24
+ const profile = await client.staff.me();
25
+ return {
26
+ content: [{ type: "text" as const, text: JSON.stringify({ success: true, profile }, null, 2) }],
27
+ };
28
+ }
29
+
30
+ if (action === "list") {
31
+ const members = await client.staff.list();
32
+ return {
33
+ content: [{ type: "text" as const, text: JSON.stringify({ success: true, members }, null, 2) }],
34
+ };
35
+ }
36
+
37
+ if (action === "create") {
38
+ if (!name) throw new Error("name is required for create action");
39
+ if (!role) throw new Error("role is required for create action");
40
+ const member = await client.staff.create({ name, email, role });
41
+ return {
42
+ content: [{
43
+ type: "text" as const,
44
+ text: JSON.stringify({
45
+ success: true, member,
46
+ note: "APIキーは一度だけ表示されます。安全に保管してください。",
47
+ }, null, 2),
48
+ }],
49
+ };
50
+ }
51
+
52
+ if (action === "get") {
53
+ if (!staffId) throw new Error("staffId is required for get action");
54
+ const member = await client.staff.get(staffId);
55
+ return {
56
+ content: [{ type: "text" as const, text: JSON.stringify({ success: true, member }, null, 2) }],
57
+ };
58
+ }
59
+
60
+ if (action === "update") {
61
+ if (!staffId) throw new Error("staffId is required for update action");
62
+ const updates: Record<string, unknown> = {};
63
+ if (name !== undefined) updates.name = name;
64
+ if (email !== undefined) updates.email = email;
65
+ if (role !== undefined) updates.role = role;
66
+ if (isActive !== undefined) updates.isActive = isActive;
67
+ const member = await client.staff.update(staffId, updates);
68
+ return {
69
+ content: [{ type: "text" as const, text: JSON.stringify({ success: true, member }, null, 2) }],
70
+ };
71
+ }
72
+
73
+ if (action === "delete") {
74
+ if (!staffId) throw new Error("staffId is required for delete action");
75
+ await client.staff.delete(staffId);
76
+ return {
77
+ content: [{ type: "text" as const, text: JSON.stringify({ success: true, deleted: staffId }, null, 2) }],
78
+ };
79
+ }
80
+
81
+ if (action === "regenerate_key") {
82
+ if (!staffId) throw new Error("staffId is required for regenerate_key action");
83
+ const result = await client.staff.regenerateKey(staffId);
84
+ return {
85
+ content: [{
86
+ type: "text" as const,
87
+ text: JSON.stringify({
88
+ success: true, staffId, newApiKey: result.apiKey,
89
+ note: "新しいAPIキーは一度だけ表示されます。安全に保管してください。",
90
+ }, null, 2),
91
+ }],
92
+ };
93
+ }
94
+
95
+ throw new Error(`Unknown action: ${action}`);
96
+ } catch (error) {
97
+ return {
98
+ content: [{ type: "text" as const, text: JSON.stringify({ success: false, error: String(error) }, null, 2) }],
99
+ isError: true,
100
+ };
101
+ }
102
+ },
103
+ );
104
+ }
@@ -1,6 +1,7 @@
1
1
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import { getClient } from "../client.js";
4
+ import { autoTrackUrls } from "./auto-track-urls.js";
4
5
 
5
6
  export function registerSendMessage(server: McpServer): void {
6
7
  server.tool(
@@ -23,9 +24,18 @@ export function registerSendMessage(server: McpServer): void {
23
24
  async ({ friendId, content, messageType }) => {
24
25
  try {
25
26
  const client = getClient();
27
+
28
+ // Auto-track URLs in flex messages
29
+ const { content: trackedContent } = await autoTrackUrls(
30
+ client,
31
+ content,
32
+ messageType,
33
+ `DM to ${friendId.slice(0, 8)}`,
34
+ );
35
+
26
36
  const result = await client.friends.sendMessage(
27
37
  friendId,
28
- content,
38
+ trackedContent,
29
39
  messageType,
30
40
  );
31
41
  return {