@line-harness/mcp-server 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +99 -3
- package/package.json +1 -1
- package/src/tools/auto-track-urls.ts +112 -0
- package/src/tools/broadcast.ts +18 -2
- package/src/tools/send-message.ts +11 -1
package/dist/index.js
CHANGED
|
@@ -29,6 +29,84 @@ function getClient() {
|
|
|
29
29
|
return clientInstance;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// src/tools/auto-track-urls.ts
|
|
33
|
+
async function autoTrackUrls(client, messageContent, messageType, title) {
|
|
34
|
+
if (messageType !== "flex") {
|
|
35
|
+
return { content: messageContent, trackedUrls: [] };
|
|
36
|
+
}
|
|
37
|
+
let parsed;
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(messageContent);
|
|
40
|
+
} catch {
|
|
41
|
+
return { content: messageContent, trackedUrls: [] };
|
|
42
|
+
}
|
|
43
|
+
const urlMap = /* @__PURE__ */ new Map();
|
|
44
|
+
collectUris(parsed, urlMap);
|
|
45
|
+
if (urlMap.size === 0) {
|
|
46
|
+
return { content: messageContent, trackedUrls: [] };
|
|
47
|
+
}
|
|
48
|
+
const trackedUrls = [];
|
|
49
|
+
for (const originalUrl of urlMap.keys()) {
|
|
50
|
+
try {
|
|
51
|
+
const link = await client.trackedLinks.create({
|
|
52
|
+
name: `${title} \u2014 ${truncate(originalUrl, 50)}`,
|
|
53
|
+
originalUrl
|
|
54
|
+
});
|
|
55
|
+
urlMap.set(originalUrl, link.trackingUrl);
|
|
56
|
+
trackedUrls.push({ original: originalUrl, tracking: link.trackingUrl });
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
replaceUris(parsed, urlMap);
|
|
61
|
+
return {
|
|
62
|
+
content: JSON.stringify(parsed),
|
|
63
|
+
trackedUrls
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function collectUris(obj, urlMap) {
|
|
67
|
+
if (obj === null || obj === void 0 || typeof obj !== "object") return;
|
|
68
|
+
if (Array.isArray(obj)) {
|
|
69
|
+
for (const item of obj) {
|
|
70
|
+
collectUris(item, urlMap);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const record = obj;
|
|
75
|
+
if (record.type === "uri" && typeof record.uri === "string") {
|
|
76
|
+
const uri = record.uri;
|
|
77
|
+
if (uri.startsWith("http://") || uri.startsWith("https://")) {
|
|
78
|
+
if (!urlMap.has(uri)) {
|
|
79
|
+
urlMap.set(uri, uri);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const value of Object.values(record)) {
|
|
84
|
+
collectUris(value, urlMap);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function replaceUris(obj, urlMap) {
|
|
88
|
+
if (obj === null || obj === void 0 || typeof obj !== "object") return;
|
|
89
|
+
if (Array.isArray(obj)) {
|
|
90
|
+
for (const item of obj) {
|
|
91
|
+
replaceUris(item, urlMap);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const record = obj;
|
|
96
|
+
if (record.type === "uri" && typeof record.uri === "string") {
|
|
97
|
+
const tracked = urlMap.get(record.uri);
|
|
98
|
+
if (tracked && tracked !== record.uri) {
|
|
99
|
+
record.uri = tracked;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const value of Object.values(record)) {
|
|
103
|
+
replaceUris(value, urlMap);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function truncate(str, max) {
|
|
107
|
+
return str.length > max ? str.slice(0, max) + "\u2026" : str;
|
|
108
|
+
}
|
|
109
|
+
|
|
32
110
|
// src/tools/send-message.ts
|
|
33
111
|
function registerSendMessage(server2) {
|
|
34
112
|
server2.tool(
|
|
@@ -46,9 +124,15 @@ function registerSendMessage(server2) {
|
|
|
46
124
|
async ({ friendId, content, messageType }) => {
|
|
47
125
|
try {
|
|
48
126
|
const client = getClient();
|
|
127
|
+
const { content: trackedContent } = await autoTrackUrls(
|
|
128
|
+
client,
|
|
129
|
+
content,
|
|
130
|
+
messageType,
|
|
131
|
+
`DM to ${friendId.slice(0, 8)}`
|
|
132
|
+
);
|
|
49
133
|
const result = await client.friends.sendMessage(
|
|
50
134
|
friendId,
|
|
51
|
-
|
|
135
|
+
trackedContent,
|
|
52
136
|
messageType
|
|
53
137
|
);
|
|
54
138
|
return {
|
|
@@ -174,10 +258,16 @@ function registerBroadcast(server2) {
|
|
|
174
258
|
isError: true
|
|
175
259
|
};
|
|
176
260
|
}
|
|
261
|
+
const { content: trackedContent2 } = await autoTrackUrls(
|
|
262
|
+
client,
|
|
263
|
+
messageContent,
|
|
264
|
+
messageType,
|
|
265
|
+
title
|
|
266
|
+
);
|
|
177
267
|
const broadcast2 = await client.broadcasts.create({
|
|
178
268
|
title: `[SEGMENT] ${title}`,
|
|
179
269
|
messageType,
|
|
180
|
-
messageContent,
|
|
270
|
+
messageContent: trackedContent2,
|
|
181
271
|
targetType: "all",
|
|
182
272
|
lineAccountId: accountId
|
|
183
273
|
});
|
|
@@ -204,10 +294,16 @@ function registerBroadcast(server2) {
|
|
|
204
294
|
throw sendError;
|
|
205
295
|
}
|
|
206
296
|
}
|
|
297
|
+
const { content: trackedContent, trackedUrls } = await autoTrackUrls(
|
|
298
|
+
client,
|
|
299
|
+
messageContent,
|
|
300
|
+
messageType,
|
|
301
|
+
title
|
|
302
|
+
);
|
|
207
303
|
const broadcast = await client.broadcasts.create({
|
|
208
304
|
title,
|
|
209
305
|
messageType,
|
|
210
|
-
messageContent,
|
|
306
|
+
messageContent: trackedContent,
|
|
211
307
|
targetType,
|
|
212
308
|
targetTagId,
|
|
213
309
|
scheduledAt,
|
package/package.json
CHANGED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { LineHarness } from "@line-harness/sdk";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Recursively find all URI action URLs in a Flex Message object,
|
|
5
|
+
* create tracked links for each, and replace with tracking URLs.
|
|
6
|
+
*/
|
|
7
|
+
export async function autoTrackUrls(
|
|
8
|
+
client: LineHarness,
|
|
9
|
+
messageContent: string,
|
|
10
|
+
messageType: string,
|
|
11
|
+
title: string,
|
|
12
|
+
): Promise<{ content: string; trackedUrls: { original: string; tracking: string }[] }> {
|
|
13
|
+
if (messageType !== "flex") {
|
|
14
|
+
return { content: messageContent, trackedUrls: [] };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let parsed: unknown;
|
|
18
|
+
try {
|
|
19
|
+
parsed = JSON.parse(messageContent);
|
|
20
|
+
} catch {
|
|
21
|
+
return { content: messageContent, trackedUrls: [] };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const urlMap = new Map<string, string>();
|
|
25
|
+
|
|
26
|
+
// Collect all unique URIs from the flex message
|
|
27
|
+
collectUris(parsed, urlMap);
|
|
28
|
+
|
|
29
|
+
if (urlMap.size === 0) {
|
|
30
|
+
return { content: messageContent, trackedUrls: [] };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Create tracked links for each unique URL
|
|
34
|
+
const trackedUrls: { original: string; tracking: string }[] = [];
|
|
35
|
+
for (const originalUrl of urlMap.keys()) {
|
|
36
|
+
try {
|
|
37
|
+
const link = await client.trackedLinks.create({
|
|
38
|
+
name: `${title} — ${truncate(originalUrl, 50)}`,
|
|
39
|
+
originalUrl,
|
|
40
|
+
});
|
|
41
|
+
urlMap.set(originalUrl, link.trackingUrl);
|
|
42
|
+
trackedUrls.push({ original: originalUrl, tracking: link.trackingUrl });
|
|
43
|
+
} catch {
|
|
44
|
+
// If tracked link creation fails, keep original URL
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Replace URLs in the parsed object
|
|
49
|
+
replaceUris(parsed, urlMap);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
content: JSON.stringify(parsed),
|
|
53
|
+
trackedUrls,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function collectUris(obj: unknown, urlMap: Map<string, string>): void {
|
|
58
|
+
if (obj === null || obj === undefined || typeof obj !== "object") return;
|
|
59
|
+
|
|
60
|
+
if (Array.isArray(obj)) {
|
|
61
|
+
for (const item of obj) {
|
|
62
|
+
collectUris(item, urlMap);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const record = obj as Record<string, unknown>;
|
|
68
|
+
|
|
69
|
+
// Check if this is a URI action
|
|
70
|
+
if (record.type === "uri" && typeof record.uri === "string") {
|
|
71
|
+
const uri = record.uri;
|
|
72
|
+
// Only track http/https URLs, skip LINE-specific URIs
|
|
73
|
+
if (uri.startsWith("http://") || uri.startsWith("https://")) {
|
|
74
|
+
if (!urlMap.has(uri)) {
|
|
75
|
+
urlMap.set(uri, uri); // placeholder, replaced after creation
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Recurse into all values
|
|
81
|
+
for (const value of Object.values(record)) {
|
|
82
|
+
collectUris(value, urlMap);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function replaceUris(obj: unknown, urlMap: Map<string, string>): void {
|
|
87
|
+
if (obj === null || obj === undefined || typeof obj !== "object") return;
|
|
88
|
+
|
|
89
|
+
if (Array.isArray(obj)) {
|
|
90
|
+
for (const item of obj) {
|
|
91
|
+
replaceUris(item, urlMap);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const record = obj as Record<string, unknown>;
|
|
97
|
+
|
|
98
|
+
if (record.type === "uri" && typeof record.uri === "string") {
|
|
99
|
+
const tracked = urlMap.get(record.uri);
|
|
100
|
+
if (tracked && tracked !== record.uri) {
|
|
101
|
+
record.uri = tracked;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const value of Object.values(record)) {
|
|
106
|
+
replaceUris(value, urlMap);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function truncate(str: string, max: number): string {
|
|
111
|
+
return str.length > max ? str.slice(0, max) + "…" : str;
|
|
112
|
+
}
|
package/src/tools/broadcast.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getClient } from "../client.js";
|
|
4
|
+
import { autoTrackUrls } from "./auto-track-urls.js";
|
|
4
5
|
|
|
5
6
|
export function registerBroadcast(server: McpServer): void {
|
|
6
7
|
server.tool(
|
|
@@ -117,10 +118,17 @@ export function registerBroadcast(server: McpServer): void {
|
|
|
117
118
|
};
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
const { content: trackedContent } = await autoTrackUrls(
|
|
122
|
+
client,
|
|
123
|
+
messageContent,
|
|
124
|
+
messageType,
|
|
125
|
+
title,
|
|
126
|
+
);
|
|
127
|
+
|
|
120
128
|
const broadcast = await client.broadcasts.create({
|
|
121
129
|
title: `[SEGMENT] ${title}`,
|
|
122
130
|
messageType,
|
|
123
|
-
messageContent,
|
|
131
|
+
messageContent: trackedContent,
|
|
124
132
|
targetType: "all",
|
|
125
133
|
lineAccountId: accountId,
|
|
126
134
|
});
|
|
@@ -148,11 +156,19 @@ export function registerBroadcast(server: McpServer): void {
|
|
|
148
156
|
}
|
|
149
157
|
}
|
|
150
158
|
|
|
159
|
+
// Auto-track URLs in flex messages
|
|
160
|
+
const { content: trackedContent, trackedUrls } = await autoTrackUrls(
|
|
161
|
+
client,
|
|
162
|
+
messageContent,
|
|
163
|
+
messageType,
|
|
164
|
+
title,
|
|
165
|
+
);
|
|
166
|
+
|
|
151
167
|
// At this point targetType is guaranteed to be 'all' or 'tag' (segment handled above)
|
|
152
168
|
const broadcast = await client.broadcasts.create({
|
|
153
169
|
title,
|
|
154
170
|
messageType,
|
|
155
|
-
messageContent,
|
|
171
|
+
messageContent: trackedContent,
|
|
156
172
|
targetType: targetType as "all" | "tag",
|
|
157
173
|
targetTagId,
|
|
158
174
|
scheduledAt,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getClient } from "../client.js";
|
|
4
|
+
import { autoTrackUrls } from "./auto-track-urls.js";
|
|
4
5
|
|
|
5
6
|
export function registerSendMessage(server: McpServer): void {
|
|
6
7
|
server.tool(
|
|
@@ -23,9 +24,18 @@ export function registerSendMessage(server: McpServer): void {
|
|
|
23
24
|
async ({ friendId, content, messageType }) => {
|
|
24
25
|
try {
|
|
25
26
|
const client = getClient();
|
|
27
|
+
|
|
28
|
+
// Auto-track URLs in flex messages
|
|
29
|
+
const { content: trackedContent } = await autoTrackUrls(
|
|
30
|
+
client,
|
|
31
|
+
content,
|
|
32
|
+
messageType,
|
|
33
|
+
`DM to ${friendId.slice(0, 8)}`,
|
|
34
|
+
);
|
|
35
|
+
|
|
26
36
|
const result = await client.friends.sendMessage(
|
|
27
37
|
friendId,
|
|
28
|
-
|
|
38
|
+
trackedContent,
|
|
29
39
|
messageType,
|
|
30
40
|
);
|
|
31
41
|
return {
|