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