@line-harness/mcp-server 0.1.1 → 0.2.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 (55) hide show
  1. package/dist/index.js +1067 -11
  2. package/package.json +2 -3
  3. package/dist/client.d.ts +0 -2
  4. package/dist/client.js +0 -22
  5. package/dist/client.js.map +0 -1
  6. package/dist/index.d.ts +0 -2
  7. package/dist/index.js.map +0 -1
  8. package/dist/resources/index.d.ts +0 -2
  9. package/dist/resources/index.js +0 -48
  10. package/dist/resources/index.js.map +0 -1
  11. package/dist/tools/account-summary.d.ts +0 -2
  12. package/dist/tools/account-summary.js +0 -47
  13. package/dist/tools/account-summary.js.map +0 -1
  14. package/dist/tools/broadcast.d.ts +0 -2
  15. package/dist/tools/broadcast.js +0 -80
  16. package/dist/tools/broadcast.js.map +0 -1
  17. package/dist/tools/create-form.d.ts +0 -2
  18. package/dist/tools/create-form.js +0 -32
  19. package/dist/tools/create-form.js.map +0 -1
  20. package/dist/tools/create-rich-menu.d.ts +0 -2
  21. package/dist/tools/create-rich-menu.js +0 -36
  22. package/dist/tools/create-rich-menu.js.map +0 -1
  23. package/dist/tools/create-scenario.d.ts +0 -2
  24. package/dist/tools/create-scenario.js +0 -78
  25. package/dist/tools/create-scenario.js.map +0 -1
  26. package/dist/tools/create-tracked-link.d.ts +0 -2
  27. package/dist/tools/create-tracked-link.js +0 -22
  28. package/dist/tools/create-tracked-link.js.map +0 -1
  29. package/dist/tools/enroll-scenario.d.ts +0 -2
  30. package/dist/tools/enroll-scenario.js +0 -23
  31. package/dist/tools/enroll-scenario.js.map +0 -1
  32. package/dist/tools/get-form-submissions.d.ts +0 -2
  33. package/dist/tools/get-form-submissions.js +0 -19
  34. package/dist/tools/get-form-submissions.js.map +0 -1
  35. package/dist/tools/get-friend-detail.d.ts +0 -2
  36. package/dist/tools/get-friend-detail.js +0 -19
  37. package/dist/tools/get-friend-detail.js.map +0 -1
  38. package/dist/tools/get-link-clicks.d.ts +0 -2
  39. package/dist/tools/get-link-clicks.js +0 -19
  40. package/dist/tools/get-link-clicks.js.map +0 -1
  41. package/dist/tools/index.d.ts +0 -2
  42. package/dist/tools/index.js +0 -31
  43. package/dist/tools/index.js.map +0 -1
  44. package/dist/tools/list-crm-objects.d.ts +0 -2
  45. package/dist/tools/list-crm-objects.js +0 -40
  46. package/dist/tools/list-crm-objects.js.map +0 -1
  47. package/dist/tools/list-friends.d.ts +0 -2
  48. package/dist/tools/list-friends.js +0 -30
  49. package/dist/tools/list-friends.js.map +0 -1
  50. package/dist/tools/manage-tags.d.ts +0 -2
  51. package/dist/tools/manage-tags.js +0 -44
  52. package/dist/tools/manage-tags.js.map +0 -1
  53. package/dist/tools/send-message.d.ts +0 -2
  54. package/dist/tools/send-message.js +0 -24
  55. package/dist/tools/send-message.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,21 +1,1077 @@
1
1
  #!/usr/bin/env node
2
+
3
+ // src/index.ts
2
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { registerAllTools } from "./tools/index.js";
5
- import { registerAllResources } from "./resources/index.js";
6
- const server = new McpServer({
7
- name: "line-harness",
8
- version: "0.1.0",
6
+
7
+ // src/tools/send-message.ts
8
+ import { z } from "zod";
9
+
10
+ // ../sdk/dist/index.mjs
11
+ var LineHarnessError = class extends Error {
12
+ constructor(message, status, endpoint) {
13
+ super(message);
14
+ this.status = status;
15
+ this.endpoint = endpoint;
16
+ this.name = "LineHarnessError";
17
+ }
18
+ };
19
+ var HttpClient = class {
20
+ baseUrl;
21
+ apiKey;
22
+ timeout;
23
+ constructor(config) {
24
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
25
+ this.apiKey = config.apiKey;
26
+ this.timeout = config.timeout;
27
+ }
28
+ async get(path) {
29
+ return this.request("GET", path);
30
+ }
31
+ async post(path, body) {
32
+ return this.request("POST", path, body);
33
+ }
34
+ async put(path, body) {
35
+ return this.request("PUT", path, body);
36
+ }
37
+ async delete(path) {
38
+ return this.request("DELETE", path);
39
+ }
40
+ async request(method, path, body) {
41
+ const url = `${this.baseUrl}${path}`;
42
+ const headers = {
43
+ Authorization: `Bearer ${this.apiKey}`,
44
+ "Content-Type": "application/json"
45
+ };
46
+ const options = {
47
+ method,
48
+ headers,
49
+ signal: AbortSignal.timeout(this.timeout)
50
+ };
51
+ if (body !== void 0) {
52
+ options.body = JSON.stringify(body);
53
+ }
54
+ const res = await fetch(url, options);
55
+ if (!res.ok) {
56
+ let errorMessage = `HTTP ${res.status}`;
57
+ try {
58
+ const errorBody = await res.json();
59
+ if (errorBody.error) errorMessage = errorBody.error;
60
+ } catch {
61
+ }
62
+ throw new LineHarnessError(errorMessage, res.status, `${method} ${path}`);
63
+ }
64
+ return res.json();
65
+ }
66
+ };
67
+ var FriendsResource = class {
68
+ constructor(http, defaultAccountId) {
69
+ this.http = http;
70
+ this.defaultAccountId = defaultAccountId;
71
+ }
72
+ async list(params) {
73
+ const query = new URLSearchParams();
74
+ if (params?.limit !== void 0) query.set("limit", String(params.limit));
75
+ if (params?.offset !== void 0) query.set("offset", String(params.offset));
76
+ if (params?.tagId) query.set("tagId", params.tagId);
77
+ const accountId = params?.accountId ?? this.defaultAccountId;
78
+ if (accountId) query.set("lineAccountId", accountId);
79
+ const qs = query.toString();
80
+ const path = qs ? `/api/friends?${qs}` : "/api/friends";
81
+ const res = await this.http.get(path);
82
+ return res.data;
83
+ }
84
+ async get(id) {
85
+ const res = await this.http.get(`/api/friends/${id}`);
86
+ return res.data;
87
+ }
88
+ async count(params) {
89
+ const accountId = params?.accountId ?? this.defaultAccountId;
90
+ const path = accountId ? `/api/friends/count?lineAccountId=${encodeURIComponent(accountId)}` : "/api/friends/count";
91
+ const res = await this.http.get(path);
92
+ return res.data.count;
93
+ }
94
+ async addTag(friendId, tagId) {
95
+ await this.http.post(`/api/friends/${friendId}/tags`, { tagId });
96
+ }
97
+ async removeTag(friendId, tagId) {
98
+ await this.http.delete(`/api/friends/${friendId}/tags/${tagId}`);
99
+ }
100
+ async sendMessage(friendId, content, messageType = "text") {
101
+ const res = await this.http.post(`/api/friends/${friendId}/messages`, {
102
+ messageType,
103
+ content
104
+ });
105
+ return res.data;
106
+ }
107
+ async setMetadata(friendId, fields) {
108
+ const res = await this.http.put(`/api/friends/${friendId}/metadata`, fields);
109
+ return res.data;
110
+ }
111
+ async setRichMenu(friendId, richMenuId) {
112
+ await this.http.post(`/api/friends/${friendId}/rich-menu`, { richMenuId });
113
+ }
114
+ async removeRichMenu(friendId) {
115
+ await this.http.delete(`/api/friends/${friendId}/rich-menu`);
116
+ }
117
+ };
118
+ var TagsResource = class {
119
+ constructor(http) {
120
+ this.http = http;
121
+ }
122
+ async list() {
123
+ const res = await this.http.get("/api/tags");
124
+ return res.data;
125
+ }
126
+ async create(input) {
127
+ const res = await this.http.post("/api/tags", input);
128
+ return res.data;
129
+ }
130
+ async delete(id) {
131
+ await this.http.delete(`/api/tags/${id}`);
132
+ }
133
+ };
134
+ var ScenariosResource = class {
135
+ constructor(http, defaultAccountId) {
136
+ this.http = http;
137
+ this.defaultAccountId = defaultAccountId;
138
+ }
139
+ async list(params) {
140
+ const accountId = params?.accountId ?? this.defaultAccountId;
141
+ const query = accountId ? `?lineAccountId=${accountId}` : "";
142
+ const res = await this.http.get(`/api/scenarios${query}`);
143
+ return res.data;
144
+ }
145
+ async get(id) {
146
+ const res = await this.http.get(`/api/scenarios/${id}`);
147
+ return res.data;
148
+ }
149
+ async create(input) {
150
+ const body = { ...input };
151
+ if (!body.lineAccountId && this.defaultAccountId) {
152
+ body.lineAccountId = this.defaultAccountId;
153
+ }
154
+ const res = await this.http.post("/api/scenarios", body);
155
+ return res.data;
156
+ }
157
+ async update(id, input) {
158
+ const res = await this.http.put(`/api/scenarios/${id}`, input);
159
+ return res.data;
160
+ }
161
+ async delete(id) {
162
+ await this.http.delete(`/api/scenarios/${id}`);
163
+ }
164
+ async addStep(scenarioId, input) {
165
+ const res = await this.http.post(`/api/scenarios/${scenarioId}/steps`, input);
166
+ return res.data;
167
+ }
168
+ async updateStep(scenarioId, stepId, input) {
169
+ const res = await this.http.put(`/api/scenarios/${scenarioId}/steps/${stepId}`, input);
170
+ return res.data;
171
+ }
172
+ async deleteStep(scenarioId, stepId) {
173
+ await this.http.delete(`/api/scenarios/${scenarioId}/steps/${stepId}`);
174
+ }
175
+ async enroll(scenarioId, friendId) {
176
+ const res = await this.http.post(
177
+ `/api/scenarios/${scenarioId}/enroll/${friendId}`
178
+ );
179
+ return res.data;
180
+ }
181
+ };
182
+ var BroadcastsResource = class {
183
+ constructor(http, defaultAccountId) {
184
+ this.http = http;
185
+ this.defaultAccountId = defaultAccountId;
186
+ }
187
+ async list(params) {
188
+ const accountId = params?.accountId ?? this.defaultAccountId;
189
+ const query = accountId ? `?lineAccountId=${accountId}` : "";
190
+ const res = await this.http.get(`/api/broadcasts${query}`);
191
+ return res.data;
192
+ }
193
+ async get(id) {
194
+ const res = await this.http.get(`/api/broadcasts/${id}`);
195
+ return res.data;
196
+ }
197
+ async create(input) {
198
+ const body = { ...input };
199
+ if (!body.lineAccountId && this.defaultAccountId) {
200
+ body.lineAccountId = this.defaultAccountId;
201
+ }
202
+ const res = await this.http.post("/api/broadcasts", body);
203
+ return res.data;
204
+ }
205
+ async update(id, input) {
206
+ const res = await this.http.put(`/api/broadcasts/${id}`, input);
207
+ return res.data;
208
+ }
209
+ async delete(id) {
210
+ await this.http.delete(`/api/broadcasts/${id}`);
211
+ }
212
+ async send(id) {
213
+ const res = await this.http.post(`/api/broadcasts/${id}/send`);
214
+ return res.data;
215
+ }
216
+ async sendToSegment(id, conditions) {
217
+ const res = await this.http.post(
218
+ `/api/broadcasts/${id}/send-segment`,
219
+ { conditions }
220
+ );
221
+ return res.data;
222
+ }
223
+ };
224
+ var RichMenusResource = class {
225
+ constructor(http) {
226
+ this.http = http;
227
+ }
228
+ async list() {
229
+ const res = await this.http.get("/api/rich-menus");
230
+ return res.data;
231
+ }
232
+ async create(menu) {
233
+ const res = await this.http.post("/api/rich-menus", menu);
234
+ return res.data;
235
+ }
236
+ async delete(richMenuId) {
237
+ await this.http.delete(`/api/rich-menus/${encodeURIComponent(richMenuId)}`);
238
+ }
239
+ async setDefault(richMenuId) {
240
+ await this.http.post(`/api/rich-menus/${encodeURIComponent(richMenuId)}/default`);
241
+ }
242
+ };
243
+ var TrackedLinksResource = class {
244
+ constructor(http) {
245
+ this.http = http;
246
+ }
247
+ async list() {
248
+ const res = await this.http.get("/api/tracked-links");
249
+ return res.data;
250
+ }
251
+ async create(input) {
252
+ const res = await this.http.post("/api/tracked-links", input);
253
+ return res.data;
254
+ }
255
+ async get(id) {
256
+ const res = await this.http.get(`/api/tracked-links/${id}`);
257
+ return res.data;
258
+ }
259
+ async delete(id) {
260
+ await this.http.delete(`/api/tracked-links/${id}`);
261
+ }
262
+ };
263
+ var FormsResource = class {
264
+ constructor(http) {
265
+ this.http = http;
266
+ }
267
+ async list() {
268
+ const res = await this.http.get("/api/forms");
269
+ return res.data;
270
+ }
271
+ async get(id) {
272
+ const res = await this.http.get(`/api/forms/${id}`);
273
+ return res.data;
274
+ }
275
+ async create(input) {
276
+ const res = await this.http.post("/api/forms", input);
277
+ return res.data;
278
+ }
279
+ async update(id, input) {
280
+ const res = await this.http.put(`/api/forms/${id}`, input);
281
+ return res.data;
282
+ }
283
+ async delete(id) {
284
+ await this.http.delete(`/api/forms/${id}`);
285
+ }
286
+ async getSubmissions(formId) {
287
+ const res = await this.http.get(
288
+ `/api/forms/${formId}/submissions`
289
+ );
290
+ return res.data;
291
+ }
292
+ };
293
+ var MULTIPLIERS = {
294
+ m: 1,
295
+ h: 60,
296
+ d: 1440,
297
+ w: 10080
298
+ };
299
+ function parseDelay(input) {
300
+ const match = input.match(/^(\d+)([mhdw])$/);
301
+ if (!match) {
302
+ throw new Error(`Invalid delay format: "${input}". Use format like "30m", "1h", "1d", "1w".`);
303
+ }
304
+ return Number(match[1]) * MULTIPLIERS[match[2]];
305
+ }
306
+ var Workflows = class {
307
+ constructor(friends, scenarios, broadcasts) {
308
+ this.friends = friends;
309
+ this.scenarios = scenarios;
310
+ this.broadcasts = broadcasts;
311
+ }
312
+ async createStepScenario(name, triggerType, steps) {
313
+ const scenario = await this.scenarios.create({ name, triggerType });
314
+ for (let i = 0; i < steps.length; i++) {
315
+ const step = steps[i];
316
+ await this.scenarios.addStep(scenario.id, {
317
+ stepOrder: i + 1,
318
+ delayMinutes: parseDelay(step.delay),
319
+ messageType: step.type,
320
+ messageContent: step.content
321
+ });
322
+ }
323
+ return this.scenarios.get(scenario.id);
324
+ }
325
+ async broadcastText(text) {
326
+ const broadcast = await this.broadcasts.create({
327
+ title: text.slice(0, 50),
328
+ messageType: "text",
329
+ messageContent: text,
330
+ targetType: "all"
331
+ });
332
+ return this.broadcasts.send(broadcast.id);
333
+ }
334
+ async broadcastToTag(tagId, messageType, content) {
335
+ const broadcast = await this.broadcasts.create({
336
+ title: content.slice(0, 50),
337
+ messageType,
338
+ messageContent: content,
339
+ targetType: "tag",
340
+ targetTagId: tagId
341
+ });
342
+ return this.broadcasts.send(broadcast.id);
343
+ }
344
+ async broadcastToSegment(messageType, content, conditions) {
345
+ const broadcast = await this.broadcasts.create({
346
+ title: content.slice(0, 50),
347
+ messageType,
348
+ messageContent: content,
349
+ targetType: "all"
350
+ });
351
+ return this.broadcasts.sendToSegment(broadcast.id, conditions);
352
+ }
353
+ async sendTextToFriend(friendId, text) {
354
+ return this.friends.sendMessage(friendId, text, "text");
355
+ }
356
+ async sendFlexToFriend(friendId, flexJson) {
357
+ return this.friends.sendMessage(friendId, flexJson, "flex");
358
+ }
359
+ };
360
+ var LineHarness = class {
361
+ friends;
362
+ tags;
363
+ scenarios;
364
+ broadcasts;
365
+ richMenus;
366
+ trackedLinks;
367
+ forms;
368
+ apiUrl;
369
+ defaultAccountId;
370
+ workflows;
371
+ createStepScenario;
372
+ broadcastText;
373
+ broadcastToTag;
374
+ broadcastToSegment;
375
+ sendTextToFriend;
376
+ sendFlexToFriend;
377
+ constructor(config) {
378
+ this.apiUrl = config.apiUrl.replace(/\/$/, "");
379
+ this.defaultAccountId = config.lineAccountId;
380
+ const http = new HttpClient({
381
+ baseUrl: this.apiUrl,
382
+ apiKey: config.apiKey,
383
+ timeout: config.timeout ?? 3e4
384
+ });
385
+ this.friends = new FriendsResource(http, this.defaultAccountId);
386
+ this.tags = new TagsResource(http);
387
+ this.scenarios = new ScenariosResource(http, this.defaultAccountId);
388
+ this.broadcasts = new BroadcastsResource(http, this.defaultAccountId);
389
+ this.richMenus = new RichMenusResource(http);
390
+ this.trackedLinks = new TrackedLinksResource(http);
391
+ this.forms = new FormsResource(http);
392
+ this.workflows = new Workflows(this.friends, this.scenarios, this.broadcasts);
393
+ this.createStepScenario = this.workflows.createStepScenario.bind(this.workflows);
394
+ this.broadcastText = this.workflows.broadcastText.bind(this.workflows);
395
+ this.broadcastToTag = this.workflows.broadcastToTag.bind(this.workflows);
396
+ this.broadcastToSegment = this.workflows.broadcastToSegment.bind(this.workflows);
397
+ this.sendTextToFriend = this.workflows.sendTextToFriend.bind(this.workflows);
398
+ this.sendFlexToFriend = this.workflows.sendFlexToFriend.bind(this.workflows);
399
+ }
400
+ /**
401
+ * Generate friend-add URL with OAuth (bot_prompt=aggressive)
402
+ * This URL does friend-add + UUID in one step.
403
+ *
404
+ * @param ref - Attribution code (e.g., 'lp-a', 'instagram', 'seminar-0322')
405
+ * @param redirect - URL to redirect after completion
406
+ */
407
+ getAuthUrl(options) {
408
+ const url = new URL(`${this.apiUrl}/auth/line`);
409
+ if (options?.ref) url.searchParams.set("ref", options.ref);
410
+ if (options?.redirect) url.searchParams.set("redirect", options.redirect);
411
+ return url.toString();
412
+ }
413
+ };
414
+
415
+ // src/client.ts
416
+ var clientInstance = null;
417
+ function getClient() {
418
+ if (clientInstance) return clientInstance;
419
+ const apiUrl = process.env.LINE_HARNESS_API_URL;
420
+ const apiKey = process.env.LINE_HARNESS_API_KEY;
421
+ const accountId = process.env.LINE_HARNESS_ACCOUNT_ID;
422
+ if (!apiUrl) {
423
+ throw new Error("LINE_HARNESS_API_URL environment variable is required");
424
+ }
425
+ if (!apiKey) {
426
+ throw new Error("LINE_HARNESS_API_KEY environment variable is required");
427
+ }
428
+ clientInstance = new LineHarness({
429
+ apiUrl,
430
+ apiKey,
431
+ lineAccountId: accountId
432
+ });
433
+ return clientInstance;
434
+ }
435
+
436
+ // src/tools/send-message.ts
437
+ function registerSendMessage(server2) {
438
+ server2.tool(
439
+ "send_message",
440
+ "Send a text or flex message to a specific friend. Use messageType 'flex' for rich card layouts.",
441
+ {
442
+ friendId: z.string().describe("The friend's ID to send the message to"),
443
+ content: z.string().describe("Message content. For text: plain string. For flex: JSON string of LINE Flex Message."),
444
+ messageType: z.enum(["text", "flex"]).default("text").describe("Message type: 'text' for plain text, 'flex' for Flex Message JSON")
445
+ },
446
+ async ({ friendId, content, messageType }) => {
447
+ try {
448
+ const client = getClient();
449
+ const result = await client.friends.sendMessage(friendId, content, messageType);
450
+ return {
451
+ content: [{
452
+ type: "text",
453
+ text: JSON.stringify({ success: true, messageId: result.messageId }, null, 2)
454
+ }]
455
+ };
456
+ } catch (error) {
457
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }, null, 2) }], isError: true };
458
+ }
459
+ }
460
+ );
461
+ }
462
+
463
+ // src/tools/broadcast.ts
464
+ import { z as z2 } from "zod";
465
+ function registerBroadcast(server2) {
466
+ server2.tool(
467
+ "broadcast",
468
+ "Send a broadcast message to all friends, a specific tag group, or a filtered segment. Creates and immediately sends the broadcast.",
469
+ {
470
+ title: z2.string().describe("Internal title for this broadcast (not shown to users)"),
471
+ messageType: z2.enum(["text", "flex"]).describe("Message type"),
472
+ messageContent: z2.string().describe("Message content. For text: plain string. For flex: JSON string."),
473
+ targetType: z2.enum(["all", "tag", "segment"]).default("all").describe("Target audience: 'all' for everyone, 'tag' for a tag group, 'segment' for filtered conditions"),
474
+ targetTagId: z2.string().optional().describe("Tag ID when targetType is 'tag'"),
475
+ segmentConditions: z2.string().optional().describe("JSON string of segment conditions when targetType is 'segment'. Format: { operator: 'AND'|'OR', rules: [{ type: 'tag_exists'|'tag_not_exists'|'metadata_equals'|'metadata_not_equals'|'ref_code'|'is_following', value: string|boolean|{key,value} }] }"),
476
+ scheduledAt: z2.string().optional().describe("ISO 8601 datetime to schedule. Omit to send immediately."),
477
+ accountId: z2.string().optional().describe("LINE account ID (uses default if omitted)")
478
+ },
479
+ async ({ title, messageType, messageContent, targetType, targetTagId, segmentConditions, scheduledAt, accountId }) => {
480
+ try {
481
+ const client = getClient();
482
+ if (targetType === "segment" && !segmentConditions) {
483
+ return {
484
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "segmentConditions is required when targetType is 'segment'" }, null, 2) }],
485
+ isError: true
486
+ };
487
+ }
488
+ if (targetType === "segment" && scheduledAt) {
489
+ return {
490
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "Scheduled segment broadcasts are not supported. Use scheduledAt only with targetType 'all' or 'tag'." }, null, 2) }],
491
+ isError: true
492
+ };
493
+ }
494
+ if (targetType === "segment" && segmentConditions) {
495
+ let parsedConditions;
496
+ try {
497
+ parsedConditions = JSON.parse(segmentConditions);
498
+ } catch {
499
+ return {
500
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "segmentConditions must be valid JSON" }, null, 2) }],
501
+ isError: true
502
+ };
503
+ }
504
+ const broadcast2 = await client.broadcasts.create({
505
+ title: `[SEGMENT] ${title}`,
506
+ messageType,
507
+ messageContent,
508
+ targetType: "all",
509
+ lineAccountId: accountId
510
+ });
511
+ try {
512
+ const result2 = await client.broadcasts.sendToSegment(broadcast2.id, parsedConditions);
513
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, broadcast: result2 }, null, 2) }] };
514
+ } catch (sendError) {
515
+ await client.broadcasts.delete(broadcast2.id).catch(() => {
516
+ });
517
+ throw sendError;
518
+ }
519
+ }
520
+ const broadcast = await client.broadcasts.create({
521
+ title,
522
+ messageType,
523
+ messageContent,
524
+ targetType,
525
+ targetTagId,
526
+ scheduledAt,
527
+ lineAccountId: accountId
528
+ });
529
+ const result = scheduledAt ? broadcast : await client.broadcasts.send(broadcast.id);
530
+ return { content: [{ type: "text", text: JSON.stringify({ success: true, broadcast: result }, null, 2) }] };
531
+ } catch (error) {
532
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }, null, 2) }], isError: true };
533
+ }
534
+ }
535
+ );
536
+ }
537
+
538
+ // src/tools/create-scenario.ts
539
+ import { z as z3 } from "zod";
540
+ function registerCreateScenario(server2) {
541
+ server2.tool(
542
+ "create_scenario",
543
+ "Create a step delivery scenario with multiple message steps. Each step has a delay and message content. Scenarios auto-trigger on friend_add, tag_added, or manual enrollment.",
544
+ {
545
+ name: z3.string().describe("Scenario name"),
546
+ triggerType: z3.enum(["friend_add", "tag_added", "manual"]).describe("When to start: 'friend_add' on new friends, 'tag_added' when a tag is applied, 'manual' for explicit enrollment"),
547
+ triggerTagId: z3.string().optional().describe("Required when triggerType is 'tag_added': the tag ID that triggers this scenario"),
548
+ steps: z3.array(z3.object({
549
+ delay: z3.string().describe("Delay before sending. Format: '0m' for immediate, '30m' for 30 minutes, '24h' for 24 hours"),
550
+ type: z3.enum(["text", "flex"]).describe("Message type"),
551
+ content: z3.string().describe("Message content")
552
+ })).describe("Ordered list of message steps"),
553
+ accountId: z3.string().optional().describe("LINE account ID (uses default if omitted)")
554
+ },
555
+ async ({ name, triggerType, triggerTagId, steps, accountId }) => {
556
+ try {
557
+ if (triggerType === "tag_added" && !triggerTagId) {
558
+ return {
559
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "triggerTagId is required when triggerType is 'tag_added'" }, null, 2) }],
560
+ isError: true
561
+ };
562
+ }
563
+ const parsedSteps = [];
564
+ for (let i = 0; i < steps.length; i++) {
565
+ const step = steps[i];
566
+ let delayMinutes;
567
+ try {
568
+ delayMinutes = parseDelay(step.delay);
569
+ } catch {
570
+ return {
571
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: `Invalid delay format at step ${i + 1}: "${step.delay}". Use formats like '0m', '30m', '24h'.` }, null, 2) }],
572
+ isError: true
573
+ };
574
+ }
575
+ parsedSteps.push({ delayMinutes, type: step.type, content: step.content });
576
+ }
577
+ const client = getClient();
578
+ const scenario = await client.scenarios.create({
579
+ name,
580
+ triggerType,
581
+ triggerTagId,
582
+ lineAccountId: accountId
583
+ });
584
+ try {
585
+ for (let i = 0; i < parsedSteps.length; i++) {
586
+ const step = parsedSteps[i];
587
+ await client.scenarios.addStep(scenario.id, {
588
+ stepOrder: i + 1,
589
+ delayMinutes: step.delayMinutes,
590
+ messageType: step.type,
591
+ messageContent: step.content
592
+ });
593
+ }
594
+ } catch (stepError) {
595
+ await client.scenarios.delete(scenario.id).catch(() => {
596
+ });
597
+ throw stepError;
598
+ }
599
+ const scenarioWithSteps = await client.scenarios.get(scenario.id);
600
+ return {
601
+ content: [{
602
+ type: "text",
603
+ text: JSON.stringify({ success: true, scenario: scenarioWithSteps }, null, 2)
604
+ }]
605
+ };
606
+ } catch (error) {
607
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }, null, 2) }], isError: true };
608
+ }
609
+ }
610
+ );
611
+ }
612
+
613
+ // src/tools/enroll-scenario.ts
614
+ import { z as z4 } from "zod";
615
+ function registerEnrollScenario(server2) {
616
+ server2.tool(
617
+ "enroll_in_scenario",
618
+ "Enroll a friend into a scenario. The friend will start receiving the scenario's step messages from step 1.",
619
+ {
620
+ scenarioId: z4.string().describe("The scenario ID to enroll the friend in"),
621
+ friendId: z4.string().describe("The friend's ID to enroll")
622
+ },
623
+ async ({ scenarioId, friendId }) => {
624
+ try {
625
+ const client = getClient();
626
+ const enrollment = await client.scenarios.enroll(scenarioId, friendId);
627
+ return {
628
+ content: [{
629
+ type: "text",
630
+ text: JSON.stringify({ success: true, enrollment }, null, 2)
631
+ }]
632
+ };
633
+ } catch (error) {
634
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }, null, 2) }], isError: true };
635
+ }
636
+ }
637
+ );
638
+ }
639
+
640
+ // src/tools/manage-tags.ts
641
+ import { z as z5 } from "zod";
642
+ function registerManageTags(server2) {
643
+ server2.tool(
644
+ "manage_tags",
645
+ "Create tags, add tags to friends, or remove tags from friends. Supports batch operations on multiple friends.",
646
+ {
647
+ action: z5.enum(["create", "add", "remove"]).describe("Action to perform"),
648
+ tagName: z5.string().optional().describe("Tag name (for 'create' action)"),
649
+ tagColor: z5.string().optional().describe("Tag color hex code (for 'create' action, e.g. '#FF0000')"),
650
+ tagId: z5.string().optional().describe("Tag ID (for 'add' or 'remove' actions)"),
651
+ friendIds: z5.array(z5.string()).optional().describe("Friend IDs to add/remove the tag from (for 'add' or 'remove' actions)")
652
+ },
653
+ async ({ action, tagName, tagColor, tagId, friendIds }) => {
654
+ try {
655
+ const client = getClient();
656
+ if (action === "create") {
657
+ if (!tagName) throw new Error("tagName is required for create action");
658
+ const tag = await client.tags.create({ name: tagName, color: tagColor });
659
+ return {
660
+ content: [{ type: "text", text: JSON.stringify({ success: true, tag }, null, 2) }]
661
+ };
662
+ }
663
+ if (!tagId) throw new Error("tagId is required for add/remove actions");
664
+ if (!friendIds?.length) throw new Error("friendIds is required for add/remove actions");
665
+ const results = [];
666
+ for (const friendId of friendIds) {
667
+ if (action === "add") {
668
+ await client.friends.addTag(friendId, tagId);
669
+ } else {
670
+ await client.friends.removeTag(friendId, tagId);
671
+ }
672
+ results.push({ friendId, status: "ok" });
673
+ }
674
+ return {
675
+ content: [{ type: "text", text: JSON.stringify({ success: true, results }, null, 2) }]
676
+ };
677
+ } catch (error) {
678
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }, null, 2) }], isError: true };
679
+ }
680
+ }
681
+ );
682
+ }
683
+
684
+ // src/tools/create-form.ts
685
+ import { z as z6 } from "zod";
686
+ function registerCreateForm(server2) {
687
+ server2.tool(
688
+ "create_form",
689
+ "Create a form for collecting user responses. Can auto-tag responders and enroll them in scenarios.",
690
+ {
691
+ name: z6.string().describe("Form name"),
692
+ description: z6.string().optional().describe("Form description shown to users"),
693
+ fields: z6.string().describe("JSON string of form fields. Format: [{ name: string, label: string, type: 'text'|'email'|'tel'|'number'|'textarea'|'select'|'radio'|'checkbox'|'date', required?: boolean, options?: string[], placeholder?: string }]"),
694
+ onSubmitTagId: z6.string().optional().describe("Tag ID to auto-apply when form is submitted"),
695
+ onSubmitScenarioId: z6.string().optional().describe("Scenario ID to auto-enroll when form is submitted"),
696
+ saveToMetadata: z6.boolean().default(true).describe("Save form responses to friend metadata"),
697
+ accountId: z6.string().optional().describe("LINE account ID (uses default if omitted)")
698
+ },
699
+ async ({ name, description, fields, onSubmitTagId, onSubmitScenarioId, saveToMetadata, accountId }) => {
700
+ try {
701
+ const client = getClient();
702
+ const form = await client.forms.create({
703
+ name,
704
+ description,
705
+ fields: JSON.parse(fields),
706
+ onSubmitTagId,
707
+ onSubmitScenarioId,
708
+ saveToMetadata
709
+ });
710
+ return {
711
+ content: [{ type: "text", text: JSON.stringify({ success: true, form }, null, 2) }]
712
+ };
713
+ } catch (error) {
714
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }, null, 2) }], isError: true };
715
+ }
716
+ }
717
+ );
718
+ }
719
+
720
+ // src/tools/create-tracked-link.ts
721
+ import { z as z7 } from "zod";
722
+ function registerCreateTrackedLink(server2) {
723
+ server2.tool(
724
+ "create_tracked_link",
725
+ "Create a click-tracking link. When clicked, can auto-tag the user and enroll them in a scenario.",
726
+ {
727
+ name: z7.string().describe("Link name (internal label)"),
728
+ originalUrl: z7.string().describe("The destination URL to redirect to"),
729
+ tagId: z7.string().optional().describe("Tag ID to auto-apply on click"),
730
+ scenarioId: z7.string().optional().describe("Scenario ID to auto-enroll on click")
731
+ },
732
+ async ({ name, originalUrl, tagId, scenarioId }) => {
733
+ try {
734
+ const client = getClient();
735
+ const link = await client.trackedLinks.create({ name, originalUrl, tagId, scenarioId });
736
+ return {
737
+ content: [{ type: "text", text: JSON.stringify({ success: true, link }, null, 2) }]
738
+ };
739
+ } catch (error) {
740
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }, null, 2) }], isError: true };
741
+ }
742
+ }
743
+ );
744
+ }
745
+
746
+ // src/tools/create-rich-menu.ts
747
+ import { z as z8 } from "zod";
748
+ function registerCreateRichMenu(server2) {
749
+ server2.tool(
750
+ "create_rich_menu",
751
+ "Create a LINE rich menu (the persistent menu at the bottom of the chat). Image must be uploaded separately via LINE Developers Console. This creates the menu structure and button areas.",
752
+ {
753
+ name: z8.string().describe("Rich menu name"),
754
+ chatBarText: z8.string().default("\u30E1\u30CB\u30E5\u30FC").describe("Text shown on the chat bar button"),
755
+ size: z8.object({
756
+ width: z8.number().default(2500).describe("Menu width in pixels (2500 for full-width)"),
757
+ height: z8.number().default(1686).describe("Menu height: 1686 for full, 843 for half")
758
+ }).default({ width: 2500, height: 1686 }).describe("Menu size in pixels"),
759
+ selected: z8.boolean().default(false).describe("Whether the rich menu is displayed by default"),
760
+ areas: z8.string().describe("JSON string of menu button areas. Format: [{ bounds: { x, y, width, height }, action: { type: 'uri'|'message'|'postback', uri?, text?, data? } }]"),
761
+ setAsDefault: z8.boolean().default(false).describe("Set this as the default rich menu for all friends")
762
+ },
763
+ async ({ name, chatBarText, size, selected, areas, setAsDefault }) => {
764
+ try {
765
+ const client = getClient();
766
+ const menu = await client.richMenus.create({
767
+ name,
768
+ chatBarText,
769
+ size,
770
+ selected,
771
+ areas: JSON.parse(areas)
772
+ });
773
+ if (setAsDefault) {
774
+ await client.richMenus.setDefault(menu.richMenuId);
775
+ }
776
+ return {
777
+ content: [{ type: "text", text: JSON.stringify({ success: true, richMenuId: menu.richMenuId, isDefault: setAsDefault }, null, 2) }]
778
+ };
779
+ } catch (error) {
780
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }, null, 2) }], isError: true };
781
+ }
782
+ }
783
+ );
784
+ }
785
+
786
+ // src/tools/list-friends.ts
787
+ import { z as z9 } from "zod";
788
+ function registerListFriends(server2) {
789
+ server2.tool(
790
+ "list_friends",
791
+ "List friends with optional filtering by tag. Returns paginated results with friend details.",
792
+ {
793
+ tagId: z9.string().optional().describe("Filter by tag ID"),
794
+ limit: z9.number().default(20).describe("Number of friends to return (max 100)"),
795
+ offset: z9.number().default(0).describe("Offset for pagination"),
796
+ accountId: z9.string().optional().describe("LINE account ID (uses default if omitted)")
797
+ },
798
+ async ({ tagId, limit, offset, accountId }) => {
799
+ try {
800
+ const client = getClient();
801
+ const result = await client.friends.list({ tagId, limit, offset, accountId });
802
+ return {
803
+ content: [{
804
+ type: "text",
805
+ text: JSON.stringify({
806
+ success: true,
807
+ total: result.total,
808
+ hasNextPage: result.hasNextPage,
809
+ friends: result.items
810
+ }, null, 2)
811
+ }]
812
+ };
813
+ } catch (error) {
814
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }, null, 2) }], isError: true };
815
+ }
816
+ }
817
+ );
818
+ }
819
+
820
+ // src/tools/get-friend-detail.ts
821
+ import { z as z10 } from "zod";
822
+ function registerGetFriendDetail(server2) {
823
+ server2.tool(
824
+ "get_friend_detail",
825
+ "Get detailed information about a specific friend including tags, metadata, and profile.",
826
+ {
827
+ friendId: z10.string().describe("The friend's ID")
828
+ },
829
+ async ({ friendId }) => {
830
+ try {
831
+ const client = getClient();
832
+ const friend = await client.friends.get(friendId);
833
+ return {
834
+ content: [{ type: "text", text: JSON.stringify({ success: true, friend }, null, 2) }]
835
+ };
836
+ } catch (error) {
837
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }, null, 2) }], isError: true };
838
+ }
839
+ }
840
+ );
841
+ }
842
+
843
+ // src/tools/get-form-submissions.ts
844
+ import { z as z11 } from "zod";
845
+ function registerGetFormSubmissions(server2) {
846
+ server2.tool(
847
+ "get_form_submissions",
848
+ "Get all submissions for a specific form. Returns response data with timestamps and friend IDs.",
849
+ {
850
+ formId: z11.string().describe("The form ID to get submissions for")
851
+ },
852
+ async ({ formId }) => {
853
+ try {
854
+ const client = getClient();
855
+ const submissions = await client.forms.getSubmissions(formId);
856
+ return {
857
+ content: [{ type: "text", text: JSON.stringify({ success: true, submissions }, null, 2) }]
858
+ };
859
+ } catch (error) {
860
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }, null, 2) }], isError: true };
861
+ }
862
+ }
863
+ );
864
+ }
865
+
866
+ // src/tools/get-link-clicks.ts
867
+ import { z as z12 } from "zod";
868
+ function registerGetLinkClicks(server2) {
869
+ server2.tool(
870
+ "get_link_clicks",
871
+ "Get click analytics for a tracked link including total clicks and per-friend click history.",
872
+ {
873
+ linkId: z12.string().describe("The tracked link ID")
874
+ },
875
+ async ({ linkId }) => {
876
+ try {
877
+ const client = getClient();
878
+ const link = await client.trackedLinks.get(linkId);
879
+ return {
880
+ content: [{ type: "text", text: JSON.stringify({ success: true, link }, null, 2) }]
881
+ };
882
+ } catch (error) {
883
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }, null, 2) }], isError: true };
884
+ }
885
+ }
886
+ );
887
+ }
888
+
889
+ // src/tools/account-summary.ts
890
+ import { z as z13 } from "zod";
891
+ function registerAccountSummary(server2) {
892
+ server2.tool(
893
+ "account_summary",
894
+ "Get a high-level summary of the LINE account: friend count, active scenarios, recent broadcasts, tags, and forms. Use this to understand the current state before making changes.",
895
+ {
896
+ accountId: z13.string().optional().describe("LINE account ID (uses default if omitted)")
897
+ },
898
+ async ({ accountId }) => {
899
+ try {
900
+ const client = getClient();
901
+ const [friendCount, scenarios, broadcasts, tags, forms] = await Promise.all([
902
+ client.friends.count({ accountId }),
903
+ client.scenarios.list({ accountId }),
904
+ client.broadcasts.list({ accountId }),
905
+ client.tags.list(),
906
+ client.forms.list()
907
+ ]);
908
+ const activeScenarios = scenarios.filter((s) => s.isActive);
909
+ const recentBroadcasts = broadcasts.slice(0, 5);
910
+ const summary = {
911
+ friends: { total: friendCount },
912
+ scenarios: {
913
+ total: scenarios.length,
914
+ active: activeScenarios.length,
915
+ activeList: activeScenarios.map((s) => ({ id: s.id, name: s.name, triggerType: s.triggerType }))
916
+ },
917
+ broadcasts: {
918
+ total: broadcasts.length,
919
+ recent: recentBroadcasts.map((b) => ({ id: b.id, title: b.title, status: b.status, sentAt: b.sentAt }))
920
+ },
921
+ tags: {
922
+ total: tags.length,
923
+ list: tags.map((t) => ({ id: t.id, name: t.name }))
924
+ },
925
+ forms: {
926
+ total: forms.length,
927
+ list: forms.map((f) => ({ id: f.id, name: f.name }))
928
+ }
929
+ };
930
+ return {
931
+ content: [{ type: "text", text: JSON.stringify(summary, null, 2) }]
932
+ };
933
+ } catch (error) {
934
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }, null, 2) }], isError: true };
935
+ }
936
+ }
937
+ );
938
+ }
939
+
940
+ // src/tools/list-crm-objects.ts
941
+ import { z as z14 } from "zod";
942
+ function registerListCrmObjects(server2) {
943
+ server2.tool(
944
+ "list_crm_objects",
945
+ "List all CRM objects of a specific type: scenarios, forms, tags, rich menus, tracked links, or broadcasts.",
946
+ {
947
+ objectType: z14.enum(["scenarios", "forms", "tags", "rich_menus", "tracked_links", "broadcasts"]).describe("Type of CRM object to list"),
948
+ accountId: z14.string().optional().describe("LINE account ID (uses default if omitted)")
949
+ },
950
+ async ({ objectType, accountId }) => {
951
+ try {
952
+ const client = getClient();
953
+ let items;
954
+ switch (objectType) {
955
+ case "scenarios":
956
+ items = await client.scenarios.list({ accountId });
957
+ break;
958
+ case "forms":
959
+ items = await client.forms.list();
960
+ break;
961
+ case "tags":
962
+ items = await client.tags.list();
963
+ break;
964
+ case "rich_menus":
965
+ items = await client.richMenus.list();
966
+ break;
967
+ case "tracked_links":
968
+ items = await client.trackedLinks.list();
969
+ break;
970
+ case "broadcasts":
971
+ items = await client.broadcasts.list({ accountId });
972
+ break;
973
+ }
974
+ return {
975
+ content: [{ type: "text", text: JSON.stringify({ success: true, objectType, items }, null, 2) }]
976
+ };
977
+ } catch (error) {
978
+ return { content: [{ type: "text", text: JSON.stringify({ success: false, error: String(error) }, null, 2) }], isError: true };
979
+ }
980
+ }
981
+ );
982
+ }
983
+
984
+ // src/tools/index.ts
985
+ function registerAllTools(server2) {
986
+ registerSendMessage(server2);
987
+ registerBroadcast(server2);
988
+ registerCreateScenario(server2);
989
+ registerEnrollScenario(server2);
990
+ registerManageTags(server2);
991
+ registerCreateForm(server2);
992
+ registerCreateTrackedLink(server2);
993
+ registerCreateRichMenu(server2);
994
+ registerListFriends(server2);
995
+ registerGetFriendDetail(server2);
996
+ registerGetFormSubmissions(server2);
997
+ registerGetLinkClicks(server2);
998
+ registerAccountSummary(server2);
999
+ registerListCrmObjects(server2);
1000
+ }
1001
+
1002
+ // src/resources/index.ts
1003
+ function registerAllResources(server2) {
1004
+ server2.resource(
1005
+ "Account Summary",
1006
+ "line-harness://account/summary",
1007
+ async (uri) => {
1008
+ const client = getClient();
1009
+ const [friendCount, scenarios, tags] = await Promise.all([
1010
+ client.friends.count(),
1011
+ client.scenarios.list(),
1012
+ client.tags.list()
1013
+ ]);
1014
+ const summary = {
1015
+ friends: friendCount,
1016
+ activeScenarios: scenarios.filter((s) => s.isActive).length,
1017
+ totalScenarios: scenarios.length,
1018
+ tags: tags.map((t) => ({ id: t.id, name: t.name }))
1019
+ };
1020
+ return {
1021
+ contents: [{
1022
+ uri: "line-harness://account/summary",
1023
+ mimeType: "application/json",
1024
+ text: JSON.stringify(summary, null, 2)
1025
+ }]
1026
+ };
1027
+ }
1028
+ );
1029
+ server2.resource(
1030
+ "Active Scenarios",
1031
+ "line-harness://scenarios/active",
1032
+ async (uri) => {
1033
+ const client = getClient();
1034
+ const scenarios = await client.scenarios.list();
1035
+ const active = scenarios.filter((s) => s.isActive);
1036
+ return {
1037
+ contents: [{
1038
+ uri: "line-harness://scenarios/active",
1039
+ mimeType: "application/json",
1040
+ text: JSON.stringify(active, null, 2)
1041
+ }]
1042
+ };
1043
+ }
1044
+ );
1045
+ server2.resource(
1046
+ "Tags List",
1047
+ "line-harness://tags/list",
1048
+ async (uri) => {
1049
+ const client = getClient();
1050
+ const tags = await client.tags.list();
1051
+ return {
1052
+ contents: [{
1053
+ uri: "line-harness://tags/list",
1054
+ mimeType: "application/json",
1055
+ text: JSON.stringify(tags, null, 2)
1056
+ }]
1057
+ };
1058
+ }
1059
+ );
1060
+ }
1061
+
1062
+ // src/index.ts
1063
+ var server = new McpServer({
1064
+ name: "line-harness",
1065
+ version: "0.1.0"
9
1066
  });
10
1067
  registerAllTools(server);
11
1068
  registerAllResources(server);
12
1069
  async function main() {
13
- const transport = new StdioServerTransport();
14
- await server.connect(transport);
15
- console.error("LINE Harness MCP Server running on stdio");
1070
+ const transport = new StdioServerTransport();
1071
+ await server.connect(transport);
1072
+ console.error("LINE Harness MCP Server running on stdio");
16
1073
  }
17
1074
  main().catch((error) => {
18
- console.error("Fatal error:", error);
19
- process.exit(1);
1075
+ console.error("Fatal error:", error);
1076
+ process.exit(1);
20
1077
  });
21
- //# sourceMappingURL=index.js.map