@line-harness/mcp-server 0.2.1 → 0.4.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 +600 -514
- package/package.json +9 -24
- package/src/client.ts +26 -0
- package/src/index.ts +23 -0
- package/src/resources/index.ts +80 -0
- package/src/tools/account-summary.ts +178 -0
- package/src/tools/broadcast.ts +195 -0
- package/src/tools/create-form.ts +80 -0
- package/src/tools/create-rich-menu.ts +90 -0
- package/src/tools/create-scenario.ts +152 -0
- package/src/tools/create-tracked-link.ts +57 -0
- package/src/tools/enroll-scenario.ts +48 -0
- package/src/tools/get-form-submissions.ts +45 -0
- package/src/tools/get-friend-detail.ts +41 -0
- package/src/tools/get-link-clicks.ts +41 -0
- package/src/tools/index.ts +32 -0
- package/src/tools/list-crm-objects.ts +80 -0
- package/src/tools/list-friends.ts +64 -0
- package/src/tools/manage-tags.ts +90 -0
- package/src/tools/send-message.ts +60 -0
- package/tsconfig.json +20 -0
- package/tsup.config.ts +12 -0
- package/README.md +0 -63
package/dist/index.js
CHANGED
|
@@ -7,412 +7,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
7
7
|
// src/tools/send-message.ts
|
|
8
8
|
import { z } from "zod";
|
|
9
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
10
|
// src/client.ts
|
|
11
|
+
import { LineHarness } from "@line-harness/sdk";
|
|
416
12
|
var clientInstance = null;
|
|
417
13
|
function getClient() {
|
|
418
14
|
if (clientInstance) return clientInstance;
|
|
@@ -440,21 +36,47 @@ function registerSendMessage(server2) {
|
|
|
440
36
|
"Send a text or flex message to a specific friend. Use messageType 'flex' for rich card layouts.",
|
|
441
37
|
{
|
|
442
38
|
friendId: z.string().describe("The friend's ID to send the message to"),
|
|
443
|
-
content: z.string().describe(
|
|
444
|
-
|
|
39
|
+
content: z.string().describe(
|
|
40
|
+
"Message content. For text: plain string. For flex: JSON string of LINE Flex Message."
|
|
41
|
+
),
|
|
42
|
+
messageType: z.enum(["text", "flex"]).default("text").describe(
|
|
43
|
+
"Message type: 'text' for plain text, 'flex' for Flex Message JSON"
|
|
44
|
+
)
|
|
445
45
|
},
|
|
446
46
|
async ({ friendId, content, messageType }) => {
|
|
447
47
|
try {
|
|
448
48
|
const client = getClient();
|
|
449
|
-
const result = await client.friends.sendMessage(
|
|
49
|
+
const result = await client.friends.sendMessage(
|
|
50
|
+
friendId,
|
|
51
|
+
content,
|
|
52
|
+
messageType
|
|
53
|
+
);
|
|
450
54
|
return {
|
|
451
|
-
content: [
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
55
|
+
content: [
|
|
56
|
+
{
|
|
57
|
+
type: "text",
|
|
58
|
+
text: JSON.stringify(
|
|
59
|
+
{ success: true, messageId: result.messageId },
|
|
60
|
+
null,
|
|
61
|
+
2
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
]
|
|
455
65
|
};
|
|
456
66
|
} catch (error) {
|
|
457
|
-
return {
|
|
67
|
+
return {
|
|
68
|
+
content: [
|
|
69
|
+
{
|
|
70
|
+
type: "text",
|
|
71
|
+
text: JSON.stringify(
|
|
72
|
+
{ success: false, error: String(error) },
|
|
73
|
+
null,
|
|
74
|
+
2
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
isError: true
|
|
79
|
+
};
|
|
458
80
|
}
|
|
459
81
|
}
|
|
460
82
|
);
|
|
@@ -469,25 +91,64 @@ function registerBroadcast(server2) {
|
|
|
469
91
|
{
|
|
470
92
|
title: z2.string().describe("Internal title for this broadcast (not shown to users)"),
|
|
471
93
|
messageType: z2.enum(["text", "flex"]).describe("Message type"),
|
|
472
|
-
messageContent: z2.string().describe(
|
|
473
|
-
|
|
94
|
+
messageContent: z2.string().describe(
|
|
95
|
+
"Message content. For text: plain string. For flex: JSON string."
|
|
96
|
+
),
|
|
97
|
+
targetType: z2.enum(["all", "tag", "segment"]).default("all").describe(
|
|
98
|
+
"Target audience: 'all' for everyone, 'tag' for a tag group, 'segment' for filtered conditions"
|
|
99
|
+
),
|
|
474
100
|
targetTagId: z2.string().optional().describe("Tag ID when targetType is 'tag'"),
|
|
475
|
-
segmentConditions: z2.string().optional().describe(
|
|
101
|
+
segmentConditions: z2.string().optional().describe(
|
|
102
|
+
"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} }] }"
|
|
103
|
+
),
|
|
476
104
|
scheduledAt: z2.string().optional().describe("ISO 8601 datetime to schedule. Omit to send immediately."),
|
|
477
105
|
accountId: z2.string().optional().describe("LINE account ID (uses default if omitted)")
|
|
478
106
|
},
|
|
479
|
-
async ({
|
|
107
|
+
async ({
|
|
108
|
+
title,
|
|
109
|
+
messageType,
|
|
110
|
+
messageContent,
|
|
111
|
+
targetType,
|
|
112
|
+
targetTagId,
|
|
113
|
+
segmentConditions,
|
|
114
|
+
scheduledAt,
|
|
115
|
+
accountId
|
|
116
|
+
}) => {
|
|
480
117
|
try {
|
|
481
118
|
const client = getClient();
|
|
482
119
|
if (targetType === "segment" && !segmentConditions) {
|
|
483
120
|
return {
|
|
484
|
-
content: [
|
|
121
|
+
content: [
|
|
122
|
+
{
|
|
123
|
+
type: "text",
|
|
124
|
+
text: JSON.stringify(
|
|
125
|
+
{
|
|
126
|
+
success: false,
|
|
127
|
+
error: "segmentConditions is required when targetType is 'segment'"
|
|
128
|
+
},
|
|
129
|
+
null,
|
|
130
|
+
2
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
],
|
|
485
134
|
isError: true
|
|
486
135
|
};
|
|
487
136
|
}
|
|
488
137
|
if (targetType === "segment" && scheduledAt) {
|
|
489
138
|
return {
|
|
490
|
-
content: [
|
|
139
|
+
content: [
|
|
140
|
+
{
|
|
141
|
+
type: "text",
|
|
142
|
+
text: JSON.stringify(
|
|
143
|
+
{
|
|
144
|
+
success: false,
|
|
145
|
+
error: "Scheduled segment broadcasts are not supported. Use scheduledAt only with targetType 'all' or 'tag'."
|
|
146
|
+
},
|
|
147
|
+
null,
|
|
148
|
+
2
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
],
|
|
491
152
|
isError: true
|
|
492
153
|
};
|
|
493
154
|
}
|
|
@@ -497,7 +158,19 @@ function registerBroadcast(server2) {
|
|
|
497
158
|
parsedConditions = JSON.parse(segmentConditions);
|
|
498
159
|
} catch {
|
|
499
160
|
return {
|
|
500
|
-
content: [
|
|
161
|
+
content: [
|
|
162
|
+
{
|
|
163
|
+
type: "text",
|
|
164
|
+
text: JSON.stringify(
|
|
165
|
+
{
|
|
166
|
+
success: false,
|
|
167
|
+
error: "segmentConditions must be valid JSON"
|
|
168
|
+
},
|
|
169
|
+
null,
|
|
170
|
+
2
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
],
|
|
501
174
|
isError: true
|
|
502
175
|
};
|
|
503
176
|
}
|
|
@@ -509,8 +182,22 @@ function registerBroadcast(server2) {
|
|
|
509
182
|
lineAccountId: accountId
|
|
510
183
|
});
|
|
511
184
|
try {
|
|
512
|
-
const result2 = await client.broadcasts.sendToSegment(
|
|
513
|
-
|
|
185
|
+
const result2 = await client.broadcasts.sendToSegment(
|
|
186
|
+
broadcast2.id,
|
|
187
|
+
parsedConditions
|
|
188
|
+
);
|
|
189
|
+
return {
|
|
190
|
+
content: [
|
|
191
|
+
{
|
|
192
|
+
type: "text",
|
|
193
|
+
text: JSON.stringify(
|
|
194
|
+
{ success: true, broadcast: result2 },
|
|
195
|
+
null,
|
|
196
|
+
2
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
};
|
|
514
201
|
} catch (sendError) {
|
|
515
202
|
await client.broadcasts.delete(broadcast2.id).catch(() => {
|
|
516
203
|
});
|
|
@@ -527,9 +214,32 @@ function registerBroadcast(server2) {
|
|
|
527
214
|
lineAccountId: accountId
|
|
528
215
|
});
|
|
529
216
|
const result = scheduledAt ? broadcast : await client.broadcasts.send(broadcast.id);
|
|
530
|
-
return {
|
|
217
|
+
return {
|
|
218
|
+
content: [
|
|
219
|
+
{
|
|
220
|
+
type: "text",
|
|
221
|
+
text: JSON.stringify(
|
|
222
|
+
{ success: true, broadcast: result },
|
|
223
|
+
null,
|
|
224
|
+
2
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
]
|
|
228
|
+
};
|
|
531
229
|
} catch (error) {
|
|
532
|
-
return {
|
|
230
|
+
return {
|
|
231
|
+
content: [
|
|
232
|
+
{
|
|
233
|
+
type: "text",
|
|
234
|
+
text: JSON.stringify(
|
|
235
|
+
{ success: false, error: String(error) },
|
|
236
|
+
null,
|
|
237
|
+
2
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
],
|
|
241
|
+
isError: true
|
|
242
|
+
};
|
|
533
243
|
}
|
|
534
244
|
}
|
|
535
245
|
);
|
|
@@ -537,26 +247,47 @@ function registerBroadcast(server2) {
|
|
|
537
247
|
|
|
538
248
|
// src/tools/create-scenario.ts
|
|
539
249
|
import { z as z3 } from "zod";
|
|
250
|
+
import { parseDelay } from "@line-harness/sdk";
|
|
540
251
|
function registerCreateScenario(server2) {
|
|
541
252
|
server2.tool(
|
|
542
253
|
"create_scenario",
|
|
543
254
|
"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
255
|
{
|
|
545
256
|
name: z3.string().describe("Scenario name"),
|
|
546
|
-
triggerType: z3.enum(["friend_add", "tag_added", "manual"]).describe(
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
257
|
+
triggerType: z3.enum(["friend_add", "tag_added", "manual"]).describe(
|
|
258
|
+
"When to start: 'friend_add' on new friends, 'tag_added' when a tag is applied, 'manual' for explicit enrollment"
|
|
259
|
+
),
|
|
260
|
+
triggerTagId: z3.string().optional().describe(
|
|
261
|
+
"Required when triggerType is 'tag_added': the tag ID that triggers this scenario"
|
|
262
|
+
),
|
|
263
|
+
steps: z3.array(
|
|
264
|
+
z3.object({
|
|
265
|
+
delay: z3.string().describe(
|
|
266
|
+
"Delay before sending. Format: '0m' for immediate, '30m' for 30 minutes, '24h' for 24 hours"
|
|
267
|
+
),
|
|
268
|
+
type: z3.enum(["text", "flex"]).describe("Message type"),
|
|
269
|
+
content: z3.string().describe("Message content")
|
|
270
|
+
})
|
|
271
|
+
).describe("Ordered list of message steps"),
|
|
553
272
|
accountId: z3.string().optional().describe("LINE account ID (uses default if omitted)")
|
|
554
273
|
},
|
|
555
274
|
async ({ name, triggerType, triggerTagId, steps, accountId }) => {
|
|
556
275
|
try {
|
|
557
276
|
if (triggerType === "tag_added" && !triggerTagId) {
|
|
558
277
|
return {
|
|
559
|
-
content: [
|
|
278
|
+
content: [
|
|
279
|
+
{
|
|
280
|
+
type: "text",
|
|
281
|
+
text: JSON.stringify(
|
|
282
|
+
{
|
|
283
|
+
success: false,
|
|
284
|
+
error: "triggerTagId is required when triggerType is 'tag_added'"
|
|
285
|
+
},
|
|
286
|
+
null,
|
|
287
|
+
2
|
|
288
|
+
)
|
|
289
|
+
}
|
|
290
|
+
],
|
|
560
291
|
isError: true
|
|
561
292
|
};
|
|
562
293
|
}
|
|
@@ -568,11 +299,27 @@ function registerCreateScenario(server2) {
|
|
|
568
299
|
delayMinutes = parseDelay(step.delay);
|
|
569
300
|
} catch {
|
|
570
301
|
return {
|
|
571
|
-
content: [
|
|
302
|
+
content: [
|
|
303
|
+
{
|
|
304
|
+
type: "text",
|
|
305
|
+
text: JSON.stringify(
|
|
306
|
+
{
|
|
307
|
+
success: false,
|
|
308
|
+
error: `Invalid delay format at step ${i + 1}: "${step.delay}". Use formats like '0m', '30m', '24h'.`
|
|
309
|
+
},
|
|
310
|
+
null,
|
|
311
|
+
2
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
],
|
|
572
315
|
isError: true
|
|
573
316
|
};
|
|
574
317
|
}
|
|
575
|
-
parsedSteps.push({
|
|
318
|
+
parsedSteps.push({
|
|
319
|
+
delayMinutes,
|
|
320
|
+
type: step.type,
|
|
321
|
+
content: step.content
|
|
322
|
+
});
|
|
576
323
|
}
|
|
577
324
|
const client = getClient();
|
|
578
325
|
const scenario = await client.scenarios.create({
|
|
@@ -598,13 +345,31 @@ function registerCreateScenario(server2) {
|
|
|
598
345
|
}
|
|
599
346
|
const scenarioWithSteps = await client.scenarios.get(scenario.id);
|
|
600
347
|
return {
|
|
601
|
-
content: [
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
348
|
+
content: [
|
|
349
|
+
{
|
|
350
|
+
type: "text",
|
|
351
|
+
text: JSON.stringify(
|
|
352
|
+
{ success: true, scenario: scenarioWithSteps },
|
|
353
|
+
null,
|
|
354
|
+
2
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
]
|
|
605
358
|
};
|
|
606
359
|
} catch (error) {
|
|
607
|
-
return {
|
|
360
|
+
return {
|
|
361
|
+
content: [
|
|
362
|
+
{
|
|
363
|
+
type: "text",
|
|
364
|
+
text: JSON.stringify(
|
|
365
|
+
{ success: false, error: String(error) },
|
|
366
|
+
null,
|
|
367
|
+
2
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
],
|
|
371
|
+
isError: true
|
|
372
|
+
};
|
|
608
373
|
}
|
|
609
374
|
}
|
|
610
375
|
);
|
|
@@ -625,13 +390,31 @@ function registerEnrollScenario(server2) {
|
|
|
625
390
|
const client = getClient();
|
|
626
391
|
const enrollment = await client.scenarios.enroll(scenarioId, friendId);
|
|
627
392
|
return {
|
|
628
|
-
content: [
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
393
|
+
content: [
|
|
394
|
+
{
|
|
395
|
+
type: "text",
|
|
396
|
+
text: JSON.stringify(
|
|
397
|
+
{ success: true, enrollment },
|
|
398
|
+
null,
|
|
399
|
+
2
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
]
|
|
632
403
|
};
|
|
633
404
|
} catch (error) {
|
|
634
|
-
return {
|
|
405
|
+
return {
|
|
406
|
+
content: [
|
|
407
|
+
{
|
|
408
|
+
type: "text",
|
|
409
|
+
text: JSON.stringify(
|
|
410
|
+
{ success: false, error: String(error) },
|
|
411
|
+
null,
|
|
412
|
+
2
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
],
|
|
416
|
+
isError: true
|
|
417
|
+
};
|
|
635
418
|
}
|
|
636
419
|
}
|
|
637
420
|
);
|
|
@@ -648,20 +431,32 @@ function registerManageTags(server2) {
|
|
|
648
431
|
tagName: z5.string().optional().describe("Tag name (for 'create' action)"),
|
|
649
432
|
tagColor: z5.string().optional().describe("Tag color hex code (for 'create' action, e.g. '#FF0000')"),
|
|
650
433
|
tagId: z5.string().optional().describe("Tag ID (for 'add' or 'remove' actions)"),
|
|
651
|
-
friendIds: z5.array(z5.string()).optional().describe(
|
|
434
|
+
friendIds: z5.array(z5.string()).optional().describe(
|
|
435
|
+
"Friend IDs to add/remove the tag from (for 'add' or 'remove' actions)"
|
|
436
|
+
)
|
|
652
437
|
},
|
|
653
438
|
async ({ action, tagName, tagColor, tagId, friendIds }) => {
|
|
654
439
|
try {
|
|
655
440
|
const client = getClient();
|
|
656
441
|
if (action === "create") {
|
|
657
442
|
if (!tagName) throw new Error("tagName is required for create action");
|
|
658
|
-
const tag = await client.tags.create({
|
|
443
|
+
const tag = await client.tags.create({
|
|
444
|
+
name: tagName,
|
|
445
|
+
color: tagColor
|
|
446
|
+
});
|
|
659
447
|
return {
|
|
660
|
-
content: [
|
|
448
|
+
content: [
|
|
449
|
+
{
|
|
450
|
+
type: "text",
|
|
451
|
+
text: JSON.stringify({ success: true, tag }, null, 2)
|
|
452
|
+
}
|
|
453
|
+
]
|
|
661
454
|
};
|
|
662
455
|
}
|
|
663
|
-
if (!tagId)
|
|
664
|
-
|
|
456
|
+
if (!tagId)
|
|
457
|
+
throw new Error("tagId is required for add/remove actions");
|
|
458
|
+
if (!friendIds?.length)
|
|
459
|
+
throw new Error("friendIds is required for add/remove actions");
|
|
665
460
|
const results = [];
|
|
666
461
|
for (const friendId of friendIds) {
|
|
667
462
|
if (action === "add") {
|
|
@@ -672,10 +467,27 @@ function registerManageTags(server2) {
|
|
|
672
467
|
results.push({ friendId, status: "ok" });
|
|
673
468
|
}
|
|
674
469
|
return {
|
|
675
|
-
content: [
|
|
470
|
+
content: [
|
|
471
|
+
{
|
|
472
|
+
type: "text",
|
|
473
|
+
text: JSON.stringify({ success: true, results }, null, 2)
|
|
474
|
+
}
|
|
475
|
+
]
|
|
676
476
|
};
|
|
677
477
|
} catch (error) {
|
|
678
|
-
return {
|
|
478
|
+
return {
|
|
479
|
+
content: [
|
|
480
|
+
{
|
|
481
|
+
type: "text",
|
|
482
|
+
text: JSON.stringify(
|
|
483
|
+
{ success: false, error: String(error) },
|
|
484
|
+
null,
|
|
485
|
+
2
|
|
486
|
+
)
|
|
487
|
+
}
|
|
488
|
+
],
|
|
489
|
+
isError: true
|
|
490
|
+
};
|
|
679
491
|
}
|
|
680
492
|
}
|
|
681
493
|
);
|
|
@@ -690,13 +502,22 @@ function registerCreateForm(server2) {
|
|
|
690
502
|
{
|
|
691
503
|
name: z6.string().describe("Form name"),
|
|
692
504
|
description: z6.string().optional().describe("Form description shown to users"),
|
|
693
|
-
fields: z6.string().describe(
|
|
505
|
+
fields: z6.string().describe(
|
|
506
|
+
"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 }]"
|
|
507
|
+
),
|
|
694
508
|
onSubmitTagId: z6.string().optional().describe("Tag ID to auto-apply when form is submitted"),
|
|
695
509
|
onSubmitScenarioId: z6.string().optional().describe("Scenario ID to auto-enroll when form is submitted"),
|
|
696
510
|
saveToMetadata: z6.boolean().default(true).describe("Save form responses to friend metadata"),
|
|
697
511
|
accountId: z6.string().optional().describe("LINE account ID (uses default if omitted)")
|
|
698
512
|
},
|
|
699
|
-
async ({
|
|
513
|
+
async ({
|
|
514
|
+
name,
|
|
515
|
+
description,
|
|
516
|
+
fields,
|
|
517
|
+
onSubmitTagId,
|
|
518
|
+
onSubmitScenarioId,
|
|
519
|
+
saveToMetadata
|
|
520
|
+
}) => {
|
|
700
521
|
try {
|
|
701
522
|
const client = getClient();
|
|
702
523
|
const form = await client.forms.create({
|
|
@@ -708,10 +529,27 @@ function registerCreateForm(server2) {
|
|
|
708
529
|
saveToMetadata
|
|
709
530
|
});
|
|
710
531
|
return {
|
|
711
|
-
content: [
|
|
532
|
+
content: [
|
|
533
|
+
{
|
|
534
|
+
type: "text",
|
|
535
|
+
text: JSON.stringify({ success: true, form }, null, 2)
|
|
536
|
+
}
|
|
537
|
+
]
|
|
712
538
|
};
|
|
713
539
|
} catch (error) {
|
|
714
|
-
return {
|
|
540
|
+
return {
|
|
541
|
+
content: [
|
|
542
|
+
{
|
|
543
|
+
type: "text",
|
|
544
|
+
text: JSON.stringify(
|
|
545
|
+
{ success: false, error: String(error) },
|
|
546
|
+
null,
|
|
547
|
+
2
|
|
548
|
+
)
|
|
549
|
+
}
|
|
550
|
+
],
|
|
551
|
+
isError: true
|
|
552
|
+
};
|
|
715
553
|
}
|
|
716
554
|
}
|
|
717
555
|
);
|
|
@@ -732,12 +570,34 @@ function registerCreateTrackedLink(server2) {
|
|
|
732
570
|
async ({ name, originalUrl, tagId, scenarioId }) => {
|
|
733
571
|
try {
|
|
734
572
|
const client = getClient();
|
|
735
|
-
const link = await client.trackedLinks.create({
|
|
573
|
+
const link = await client.trackedLinks.create({
|
|
574
|
+
name,
|
|
575
|
+
originalUrl,
|
|
576
|
+
tagId,
|
|
577
|
+
scenarioId
|
|
578
|
+
});
|
|
736
579
|
return {
|
|
737
|
-
content: [
|
|
580
|
+
content: [
|
|
581
|
+
{
|
|
582
|
+
type: "text",
|
|
583
|
+
text: JSON.stringify({ success: true, link }, null, 2)
|
|
584
|
+
}
|
|
585
|
+
]
|
|
738
586
|
};
|
|
739
587
|
} catch (error) {
|
|
740
|
-
return {
|
|
588
|
+
return {
|
|
589
|
+
content: [
|
|
590
|
+
{
|
|
591
|
+
type: "text",
|
|
592
|
+
text: JSON.stringify(
|
|
593
|
+
{ success: false, error: String(error) },
|
|
594
|
+
null,
|
|
595
|
+
2
|
|
596
|
+
)
|
|
597
|
+
}
|
|
598
|
+
],
|
|
599
|
+
isError: true
|
|
600
|
+
};
|
|
741
601
|
}
|
|
742
602
|
}
|
|
743
603
|
);
|
|
@@ -757,7 +617,9 @@ function registerCreateRichMenu(server2) {
|
|
|
757
617
|
height: z8.number().default(1686).describe("Menu height: 1686 for full, 843 for half")
|
|
758
618
|
}).default({ width: 2500, height: 1686 }).describe("Menu size in pixels"),
|
|
759
619
|
selected: z8.boolean().default(false).describe("Whether the rich menu is displayed by default"),
|
|
760
|
-
areas: z8.string().describe(
|
|
620
|
+
areas: z8.string().describe(
|
|
621
|
+
"JSON string of menu button areas. Format: [{ bounds: { x, y, width, height }, action: { type: 'uri'|'message'|'postback', uri?, text?, data? } }]"
|
|
622
|
+
),
|
|
761
623
|
setAsDefault: z8.boolean().default(false).describe("Set this as the default rich menu for all friends")
|
|
762
624
|
},
|
|
763
625
|
async ({ name, chatBarText, size, selected, areas, setAsDefault }) => {
|
|
@@ -774,10 +636,35 @@ function registerCreateRichMenu(server2) {
|
|
|
774
636
|
await client.richMenus.setDefault(menu.richMenuId);
|
|
775
637
|
}
|
|
776
638
|
return {
|
|
777
|
-
content: [
|
|
639
|
+
content: [
|
|
640
|
+
{
|
|
641
|
+
type: "text",
|
|
642
|
+
text: JSON.stringify(
|
|
643
|
+
{
|
|
644
|
+
success: true,
|
|
645
|
+
richMenuId: menu.richMenuId,
|
|
646
|
+
isDefault: setAsDefault
|
|
647
|
+
},
|
|
648
|
+
null,
|
|
649
|
+
2
|
|
650
|
+
)
|
|
651
|
+
}
|
|
652
|
+
]
|
|
778
653
|
};
|
|
779
654
|
} catch (error) {
|
|
780
|
-
return {
|
|
655
|
+
return {
|
|
656
|
+
content: [
|
|
657
|
+
{
|
|
658
|
+
type: "text",
|
|
659
|
+
text: JSON.stringify(
|
|
660
|
+
{ success: false, error: String(error) },
|
|
661
|
+
null,
|
|
662
|
+
2
|
|
663
|
+
)
|
|
664
|
+
}
|
|
665
|
+
],
|
|
666
|
+
isError: true
|
|
667
|
+
};
|
|
781
668
|
}
|
|
782
669
|
}
|
|
783
670
|
);
|
|
@@ -798,20 +685,43 @@ function registerListFriends(server2) {
|
|
|
798
685
|
async ({ tagId, limit, offset, accountId }) => {
|
|
799
686
|
try {
|
|
800
687
|
const client = getClient();
|
|
801
|
-
const result = await client.friends.list({
|
|
688
|
+
const result = await client.friends.list({
|
|
689
|
+
tagId,
|
|
690
|
+
limit,
|
|
691
|
+
offset,
|
|
692
|
+
accountId
|
|
693
|
+
});
|
|
802
694
|
return {
|
|
803
|
-
content: [
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
695
|
+
content: [
|
|
696
|
+
{
|
|
697
|
+
type: "text",
|
|
698
|
+
text: JSON.stringify(
|
|
699
|
+
{
|
|
700
|
+
success: true,
|
|
701
|
+
total: result.total,
|
|
702
|
+
hasNextPage: result.hasNextPage,
|
|
703
|
+
friends: result.items
|
|
704
|
+
},
|
|
705
|
+
null,
|
|
706
|
+
2
|
|
707
|
+
)
|
|
708
|
+
}
|
|
709
|
+
]
|
|
812
710
|
};
|
|
813
711
|
} catch (error) {
|
|
814
|
-
return {
|
|
712
|
+
return {
|
|
713
|
+
content: [
|
|
714
|
+
{
|
|
715
|
+
type: "text",
|
|
716
|
+
text: JSON.stringify(
|
|
717
|
+
{ success: false, error: String(error) },
|
|
718
|
+
null,
|
|
719
|
+
2
|
|
720
|
+
)
|
|
721
|
+
}
|
|
722
|
+
],
|
|
723
|
+
isError: true
|
|
724
|
+
};
|
|
815
725
|
}
|
|
816
726
|
}
|
|
817
727
|
);
|
|
@@ -831,10 +741,27 @@ function registerGetFriendDetail(server2) {
|
|
|
831
741
|
const client = getClient();
|
|
832
742
|
const friend = await client.friends.get(friendId);
|
|
833
743
|
return {
|
|
834
|
-
content: [
|
|
744
|
+
content: [
|
|
745
|
+
{
|
|
746
|
+
type: "text",
|
|
747
|
+
text: JSON.stringify({ success: true, friend }, null, 2)
|
|
748
|
+
}
|
|
749
|
+
]
|
|
835
750
|
};
|
|
836
751
|
} catch (error) {
|
|
837
|
-
return {
|
|
752
|
+
return {
|
|
753
|
+
content: [
|
|
754
|
+
{
|
|
755
|
+
type: "text",
|
|
756
|
+
text: JSON.stringify(
|
|
757
|
+
{ success: false, error: String(error) },
|
|
758
|
+
null,
|
|
759
|
+
2
|
|
760
|
+
)
|
|
761
|
+
}
|
|
762
|
+
],
|
|
763
|
+
isError: true
|
|
764
|
+
};
|
|
838
765
|
}
|
|
839
766
|
}
|
|
840
767
|
);
|
|
@@ -854,10 +781,31 @@ function registerGetFormSubmissions(server2) {
|
|
|
854
781
|
const client = getClient();
|
|
855
782
|
const submissions = await client.forms.getSubmissions(formId);
|
|
856
783
|
return {
|
|
857
|
-
content: [
|
|
784
|
+
content: [
|
|
785
|
+
{
|
|
786
|
+
type: "text",
|
|
787
|
+
text: JSON.stringify(
|
|
788
|
+
{ success: true, submissions },
|
|
789
|
+
null,
|
|
790
|
+
2
|
|
791
|
+
)
|
|
792
|
+
}
|
|
793
|
+
]
|
|
858
794
|
};
|
|
859
795
|
} catch (error) {
|
|
860
|
-
return {
|
|
796
|
+
return {
|
|
797
|
+
content: [
|
|
798
|
+
{
|
|
799
|
+
type: "text",
|
|
800
|
+
text: JSON.stringify(
|
|
801
|
+
{ success: false, error: String(error) },
|
|
802
|
+
null,
|
|
803
|
+
2
|
|
804
|
+
)
|
|
805
|
+
}
|
|
806
|
+
],
|
|
807
|
+
isError: true
|
|
808
|
+
};
|
|
861
809
|
}
|
|
862
810
|
}
|
|
863
811
|
);
|
|
@@ -877,10 +825,27 @@ function registerGetLinkClicks(server2) {
|
|
|
877
825
|
const client = getClient();
|
|
878
826
|
const link = await client.trackedLinks.get(linkId);
|
|
879
827
|
return {
|
|
880
|
-
content: [
|
|
828
|
+
content: [
|
|
829
|
+
{
|
|
830
|
+
type: "text",
|
|
831
|
+
text: JSON.stringify({ success: true, link }, null, 2)
|
|
832
|
+
}
|
|
833
|
+
]
|
|
881
834
|
};
|
|
882
835
|
} catch (error) {
|
|
883
|
-
return {
|
|
836
|
+
return {
|
|
837
|
+
content: [
|
|
838
|
+
{
|
|
839
|
+
type: "text",
|
|
840
|
+
text: JSON.stringify(
|
|
841
|
+
{ success: false, error: String(error) },
|
|
842
|
+
null,
|
|
843
|
+
2
|
|
844
|
+
)
|
|
845
|
+
}
|
|
846
|
+
],
|
|
847
|
+
isError: true
|
|
848
|
+
};
|
|
884
849
|
}
|
|
885
850
|
}
|
|
886
851
|
);
|
|
@@ -891,47 +856,127 @@ import { z as z13 } from "zod";
|
|
|
891
856
|
function registerAccountSummary(server2) {
|
|
892
857
|
server2.tool(
|
|
893
858
|
"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.",
|
|
859
|
+
"Get a high-level summary of the LINE account: friend count per account (DB + LINE API stats), active scenarios, recent broadcasts, tags, and forms. Use this to understand the current state before making changes.",
|
|
895
860
|
{
|
|
896
861
|
accountId: z13.string().optional().describe("LINE account ID (uses default if omitted)")
|
|
897
862
|
},
|
|
898
863
|
async ({ accountId }) => {
|
|
899
864
|
try {
|
|
900
865
|
const client = getClient();
|
|
901
|
-
const
|
|
902
|
-
|
|
866
|
+
const apiUrl = process.env.LINE_HARNESS_API_URL;
|
|
867
|
+
const apiKey = process.env.LINE_HARNESS_API_KEY;
|
|
868
|
+
const accountsRes = await fetch(`${apiUrl}/api/line-accounts`, {
|
|
869
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
870
|
+
});
|
|
871
|
+
const accountsData = await accountsRes.json();
|
|
872
|
+
const accounts = accountsData.success ? accountsData.data : [];
|
|
873
|
+
const accountStats = [];
|
|
874
|
+
for (const acc of accounts) {
|
|
875
|
+
const countRes = await fetch(
|
|
876
|
+
`${apiUrl}/api/friends/count?lineAccountId=${encodeURIComponent(acc.id)}`,
|
|
877
|
+
{ headers: { Authorization: `Bearer ${apiKey}` } }
|
|
878
|
+
);
|
|
879
|
+
const countData = await countRes.json();
|
|
880
|
+
const count = countData.success ? countData.data.count : 0;
|
|
881
|
+
accountStats.push({
|
|
882
|
+
id: acc.id,
|
|
883
|
+
name: acc.name,
|
|
884
|
+
channelId: acc.channelId,
|
|
885
|
+
friendsInDb: count
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
for (const acc of accountStats) {
|
|
889
|
+
try {
|
|
890
|
+
const healthRes = await fetch(
|
|
891
|
+
`${apiUrl}/api/accounts/${acc.id}/health`,
|
|
892
|
+
{ headers: { Authorization: `Bearer ${apiKey}` } }
|
|
893
|
+
);
|
|
894
|
+
const healthData = await healthRes.json();
|
|
895
|
+
if (healthData.success) {
|
|
896
|
+
acc.riskLevel = healthData.data.riskLevel;
|
|
897
|
+
}
|
|
898
|
+
} catch {
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
const [totalFriends, scenarios, broadcasts, tags, forms] = await Promise.all([
|
|
902
|
+
client.friends.count(),
|
|
903
903
|
client.scenarios.list({ accountId }),
|
|
904
904
|
client.broadcasts.list({ accountId }),
|
|
905
905
|
client.tags.list(),
|
|
906
906
|
client.forms.list()
|
|
907
907
|
]);
|
|
908
|
-
const activeScenarios = scenarios.filter(
|
|
908
|
+
const activeScenarios = scenarios.filter(
|
|
909
|
+
(s) => s.isActive
|
|
910
|
+
);
|
|
909
911
|
const recentBroadcasts = broadcasts.slice(0, 5);
|
|
910
912
|
const summary = {
|
|
911
|
-
friends: {
|
|
913
|
+
friends: {
|
|
914
|
+
totalDbRecords: totalFriends,
|
|
915
|
+
note: "totalDbRecords includes both Account \u2460 and \u2461 records. Same user on different accounts = separate records. Use per-account counts below for accurate numbers.",
|
|
916
|
+
perAccount: accountStats
|
|
917
|
+
},
|
|
912
918
|
scenarios: {
|
|
913
919
|
total: scenarios.length,
|
|
914
920
|
active: activeScenarios.length,
|
|
915
|
-
activeList: activeScenarios.map(
|
|
921
|
+
activeList: activeScenarios.map(
|
|
922
|
+
(s) => ({
|
|
923
|
+
id: s.id,
|
|
924
|
+
name: s.name,
|
|
925
|
+
triggerType: s.triggerType
|
|
926
|
+
})
|
|
927
|
+
)
|
|
916
928
|
},
|
|
917
929
|
broadcasts: {
|
|
918
930
|
total: broadcasts.length,
|
|
919
|
-
recent: recentBroadcasts.map(
|
|
931
|
+
recent: recentBroadcasts.map(
|
|
932
|
+
(b) => ({
|
|
933
|
+
id: b.id,
|
|
934
|
+
title: b.title,
|
|
935
|
+
status: b.status,
|
|
936
|
+
sentAt: b.sentAt
|
|
937
|
+
})
|
|
938
|
+
)
|
|
920
939
|
},
|
|
921
940
|
tags: {
|
|
922
941
|
total: tags.length,
|
|
923
|
-
list: tags.map((t) => ({
|
|
942
|
+
list: tags.map((t) => ({
|
|
943
|
+
id: t.id,
|
|
944
|
+
name: t.name
|
|
945
|
+
}))
|
|
924
946
|
},
|
|
925
947
|
forms: {
|
|
926
948
|
total: forms.length,
|
|
927
|
-
list: forms.map(
|
|
949
|
+
list: forms.map(
|
|
950
|
+
(f) => ({
|
|
951
|
+
id: f.id,
|
|
952
|
+
name: f.name,
|
|
953
|
+
submitCount: f.submitCount
|
|
954
|
+
})
|
|
955
|
+
)
|
|
928
956
|
}
|
|
929
957
|
};
|
|
930
958
|
return {
|
|
931
|
-
content: [
|
|
959
|
+
content: [
|
|
960
|
+
{
|
|
961
|
+
type: "text",
|
|
962
|
+
text: JSON.stringify(summary, null, 2)
|
|
963
|
+
}
|
|
964
|
+
]
|
|
932
965
|
};
|
|
933
966
|
} catch (error) {
|
|
934
|
-
return {
|
|
967
|
+
return {
|
|
968
|
+
content: [
|
|
969
|
+
{
|
|
970
|
+
type: "text",
|
|
971
|
+
text: JSON.stringify(
|
|
972
|
+
{ success: false, error: String(error) },
|
|
973
|
+
null,
|
|
974
|
+
2
|
|
975
|
+
)
|
|
976
|
+
}
|
|
977
|
+
],
|
|
978
|
+
isError: true
|
|
979
|
+
};
|
|
935
980
|
}
|
|
936
981
|
}
|
|
937
982
|
);
|
|
@@ -944,7 +989,14 @@ function registerListCrmObjects(server2) {
|
|
|
944
989
|
"list_crm_objects",
|
|
945
990
|
"List all CRM objects of a specific type: scenarios, forms, tags, rich menus, tracked links, or broadcasts.",
|
|
946
991
|
{
|
|
947
|
-
objectType: z14.enum([
|
|
992
|
+
objectType: z14.enum([
|
|
993
|
+
"scenarios",
|
|
994
|
+
"forms",
|
|
995
|
+
"tags",
|
|
996
|
+
"rich_menus",
|
|
997
|
+
"tracked_links",
|
|
998
|
+
"broadcasts"
|
|
999
|
+
]).describe("Type of CRM object to list"),
|
|
948
1000
|
accountId: z14.string().optional().describe("LINE account ID (uses default if omitted)")
|
|
949
1001
|
},
|
|
950
1002
|
async ({ objectType, accountId }) => {
|
|
@@ -972,10 +1024,31 @@ function registerListCrmObjects(server2) {
|
|
|
972
1024
|
break;
|
|
973
1025
|
}
|
|
974
1026
|
return {
|
|
975
|
-
content: [
|
|
1027
|
+
content: [
|
|
1028
|
+
{
|
|
1029
|
+
type: "text",
|
|
1030
|
+
text: JSON.stringify(
|
|
1031
|
+
{ success: true, objectType, items },
|
|
1032
|
+
null,
|
|
1033
|
+
2
|
|
1034
|
+
)
|
|
1035
|
+
}
|
|
1036
|
+
]
|
|
976
1037
|
};
|
|
977
1038
|
} catch (error) {
|
|
978
|
-
return {
|
|
1039
|
+
return {
|
|
1040
|
+
content: [
|
|
1041
|
+
{
|
|
1042
|
+
type: "text",
|
|
1043
|
+
text: JSON.stringify(
|
|
1044
|
+
{ success: false, error: String(error) },
|
|
1045
|
+
null,
|
|
1046
|
+
2
|
|
1047
|
+
)
|
|
1048
|
+
}
|
|
1049
|
+
],
|
|
1050
|
+
isError: true
|
|
1051
|
+
};
|
|
979
1052
|
}
|
|
980
1053
|
}
|
|
981
1054
|
);
|
|
@@ -1004,7 +1077,7 @@ function registerAllResources(server2) {
|
|
|
1004
1077
|
server2.resource(
|
|
1005
1078
|
"Account Summary",
|
|
1006
1079
|
"line-harness://account/summary",
|
|
1007
|
-
async (
|
|
1080
|
+
async (_uri) => {
|
|
1008
1081
|
const client = getClient();
|
|
1009
1082
|
const [friendCount, scenarios, tags] = await Promise.all([
|
|
1010
1083
|
client.friends.count(),
|
|
@@ -1013,47 +1086,60 @@ function registerAllResources(server2) {
|
|
|
1013
1086
|
]);
|
|
1014
1087
|
const summary = {
|
|
1015
1088
|
friends: friendCount,
|
|
1016
|
-
activeScenarios: scenarios.filter(
|
|
1089
|
+
activeScenarios: scenarios.filter(
|
|
1090
|
+
(s) => s.isActive
|
|
1091
|
+
).length,
|
|
1017
1092
|
totalScenarios: scenarios.length,
|
|
1018
|
-
tags: tags.map((t) => ({
|
|
1093
|
+
tags: tags.map((t) => ({
|
|
1094
|
+
id: t.id,
|
|
1095
|
+
name: t.name
|
|
1096
|
+
}))
|
|
1019
1097
|
};
|
|
1020
1098
|
return {
|
|
1021
|
-
contents: [
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1099
|
+
contents: [
|
|
1100
|
+
{
|
|
1101
|
+
uri: "line-harness://account/summary",
|
|
1102
|
+
mimeType: "application/json",
|
|
1103
|
+
text: JSON.stringify(summary, null, 2)
|
|
1104
|
+
}
|
|
1105
|
+
]
|
|
1026
1106
|
};
|
|
1027
1107
|
}
|
|
1028
1108
|
);
|
|
1029
1109
|
server2.resource(
|
|
1030
1110
|
"Active Scenarios",
|
|
1031
1111
|
"line-harness://scenarios/active",
|
|
1032
|
-
async (
|
|
1112
|
+
async (_uri) => {
|
|
1033
1113
|
const client = getClient();
|
|
1034
1114
|
const scenarios = await client.scenarios.list();
|
|
1035
|
-
const active = scenarios.filter(
|
|
1115
|
+
const active = scenarios.filter(
|
|
1116
|
+
(s) => s.isActive
|
|
1117
|
+
);
|
|
1036
1118
|
return {
|
|
1037
|
-
contents: [
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1119
|
+
contents: [
|
|
1120
|
+
{
|
|
1121
|
+
uri: "line-harness://scenarios/active",
|
|
1122
|
+
mimeType: "application/json",
|
|
1123
|
+
text: JSON.stringify(active, null, 2)
|
|
1124
|
+
}
|
|
1125
|
+
]
|
|
1042
1126
|
};
|
|
1043
1127
|
}
|
|
1044
1128
|
);
|
|
1045
1129
|
server2.resource(
|
|
1046
1130
|
"Tags List",
|
|
1047
1131
|
"line-harness://tags/list",
|
|
1048
|
-
async (
|
|
1132
|
+
async (_uri) => {
|
|
1049
1133
|
const client = getClient();
|
|
1050
1134
|
const tags = await client.tags.list();
|
|
1051
1135
|
return {
|
|
1052
|
-
contents: [
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1136
|
+
contents: [
|
|
1137
|
+
{
|
|
1138
|
+
uri: "line-harness://tags/list",
|
|
1139
|
+
mimeType: "application/json",
|
|
1140
|
+
text: JSON.stringify(tags, null, 2)
|
|
1141
|
+
}
|
|
1142
|
+
]
|
|
1057
1143
|
};
|
|
1058
1144
|
}
|
|
1059
1145
|
);
|
|
@@ -1062,7 +1148,7 @@ function registerAllResources(server2) {
|
|
|
1062
1148
|
// src/index.ts
|
|
1063
1149
|
var server = new McpServer({
|
|
1064
1150
|
name: "line-harness",
|
|
1065
|
-
version: "0.
|
|
1151
|
+
version: "0.3.0"
|
|
1066
1152
|
});
|
|
1067
1153
|
registerAllTools(server);
|
|
1068
1154
|
registerAllResources(server);
|