@line-harness/mcp-server 0.2.2 → 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/package.json CHANGED
@@ -1,37 +1,22 @@
1
1
  {
2
2
  "name": "@line-harness/mcp-server",
3
- "version": "0.2.2",
4
- "description": "MCP Server for LINE Harness — operate LINE official accounts from any AI tool",
3
+ "version": "0.4.0",
5
4
  "type": "module",
6
- "main": "./dist/index.js",
7
5
  "bin": {
8
6
  "line-harness-mcp": "./dist/index.js"
9
7
  },
8
+ "main": "./dist/index.js",
10
9
  "scripts": {
11
- "build": "npx esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@modelcontextprotocol/sdk --external:zod && chmod 755 dist/index.js",
12
- "dev": "tsc --watch",
13
- "start": "node dist/index.js"
14
- },
15
- "keywords": [
16
- "mcp",
17
- "line",
18
- "crm",
19
- "ai"
20
- ],
21
- "license": "MIT",
22
- "publishConfig": {
23
- "access": "public"
10
+ "build": "tsup",
11
+ "dev": "tsup --watch"
24
12
  },
25
- "files": [
26
- "dist",
27
- "README.md"
28
- ],
29
13
  "dependencies": {
30
- "@modelcontextprotocol/sdk": "^1.0.0",
31
- "zod": "^3.23.0"
14
+ "@line-harness/sdk": "workspace:*",
15
+ "@modelcontextprotocol/sdk": "^1.12.1",
16
+ "zod": "^3.24.4"
32
17
  },
33
18
  "devDependencies": {
34
- "typescript": "^5.5.0",
35
- "@types/node": "^20.0.0"
19
+ "tsup": "^8.4.0",
20
+ "typescript": "^5.9.3"
36
21
  }
37
22
  }
package/src/client.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { LineHarness } from "@line-harness/sdk";
2
+
3
+ let clientInstance: LineHarness | null = null;
4
+
5
+ export function getClient(): LineHarness {
6
+ if (clientInstance) return clientInstance;
7
+
8
+ const apiUrl = process.env.LINE_HARNESS_API_URL;
9
+ const apiKey = process.env.LINE_HARNESS_API_KEY;
10
+ const accountId = process.env.LINE_HARNESS_ACCOUNT_ID;
11
+
12
+ if (!apiUrl) {
13
+ throw new Error("LINE_HARNESS_API_URL environment variable is required");
14
+ }
15
+ if (!apiKey) {
16
+ throw new Error("LINE_HARNESS_API_KEY environment variable is required");
17
+ }
18
+
19
+ clientInstance = new LineHarness({
20
+ apiUrl,
21
+ apiKey,
22
+ lineAccountId: accountId,
23
+ });
24
+
25
+ return clientInstance;
26
+ }
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { registerAllTools } from "./tools/index.js";
4
+ import { registerAllResources } from "./resources/index.js";
5
+
6
+ const server = new McpServer({
7
+ name: "line-harness",
8
+ version: "0.3.0",
9
+ });
10
+
11
+ registerAllTools(server);
12
+ registerAllResources(server);
13
+
14
+ async function main() {
15
+ const transport = new StdioServerTransport();
16
+ await server.connect(transport);
17
+ console.error("LINE Harness MCP Server running on stdio");
18
+ }
19
+
20
+ main().catch((error) => {
21
+ console.error("Fatal error:", error);
22
+ process.exit(1);
23
+ });
@@ -0,0 +1,80 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { getClient } from "../client.js";
3
+
4
+ export function registerAllResources(server: McpServer): void {
5
+ server.resource(
6
+ "Account Summary",
7
+ "line-harness://account/summary",
8
+ async (_uri) => {
9
+ const client = getClient();
10
+ const [friendCount, scenarios, tags] = await Promise.all([
11
+ client.friends.count(),
12
+ client.scenarios.list(),
13
+ client.tags.list(),
14
+ ]);
15
+
16
+ const summary = {
17
+ friends: friendCount,
18
+ activeScenarios: scenarios.filter(
19
+ (s: { isActive: boolean }) => s.isActive,
20
+ ).length,
21
+ totalScenarios: scenarios.length,
22
+ tags: tags.map((t: { id: string; name: string }) => ({
23
+ id: t.id,
24
+ name: t.name,
25
+ })),
26
+ };
27
+
28
+ return {
29
+ contents: [
30
+ {
31
+ uri: "line-harness://account/summary",
32
+ mimeType: "application/json",
33
+ text: JSON.stringify(summary, null, 2),
34
+ },
35
+ ],
36
+ };
37
+ },
38
+ );
39
+
40
+ server.resource(
41
+ "Active Scenarios",
42
+ "line-harness://scenarios/active",
43
+ async (_uri) => {
44
+ const client = getClient();
45
+ const scenarios = await client.scenarios.list();
46
+ const active = scenarios.filter(
47
+ (s: { isActive: boolean }) => s.isActive,
48
+ );
49
+
50
+ return {
51
+ contents: [
52
+ {
53
+ uri: "line-harness://scenarios/active",
54
+ mimeType: "application/json",
55
+ text: JSON.stringify(active, null, 2),
56
+ },
57
+ ],
58
+ };
59
+ },
60
+ );
61
+
62
+ server.resource(
63
+ "Tags List",
64
+ "line-harness://tags/list",
65
+ async (_uri) => {
66
+ const client = getClient();
67
+ const tags = await client.tags.list();
68
+
69
+ return {
70
+ contents: [
71
+ {
72
+ uri: "line-harness://tags/list",
73
+ mimeType: "application/json",
74
+ text: JSON.stringify(tags, null, 2),
75
+ },
76
+ ],
77
+ };
78
+ },
79
+ );
80
+ }
@@ -0,0 +1,178 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { getClient } from "../client.js";
4
+
5
+ interface AccountInfo {
6
+ id: string;
7
+ name: string;
8
+ channelId: string;
9
+ }
10
+
11
+ interface AccountStat {
12
+ id: string;
13
+ name: string;
14
+ channelId: string;
15
+ friendsInDb: number;
16
+ riskLevel?: string;
17
+ }
18
+
19
+ export function registerAccountSummary(server: McpServer): void {
20
+ server.tool(
21
+ "account_summary",
22
+ "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.",
23
+ {
24
+ accountId: z
25
+ .string()
26
+ .optional()
27
+ .describe("LINE account ID (uses default if omitted)"),
28
+ },
29
+ async ({ accountId }) => {
30
+ try {
31
+ const client = getClient();
32
+ const apiUrl = process.env.LINE_HARNESS_API_URL;
33
+ const apiKey = process.env.LINE_HARNESS_API_KEY;
34
+
35
+ // Fetch all LINE accounts
36
+ const accountsRes = await fetch(`${apiUrl}/api/line-accounts`, {
37
+ headers: { Authorization: `Bearer ${apiKey}` },
38
+ });
39
+ const accountsData = (await accountsRes.json()) as {
40
+ success: boolean;
41
+ data: AccountInfo[];
42
+ };
43
+ const accounts: AccountInfo[] = accountsData.success
44
+ ? accountsData.data
45
+ : [];
46
+
47
+ // Get per-account friend counts
48
+ const accountStats: AccountStat[] = [];
49
+ for (const acc of accounts) {
50
+ // Use direct API call for per-account count (SDK count() has no params)
51
+ const countRes = await fetch(
52
+ `${apiUrl}/api/friends/count?lineAccountId=${encodeURIComponent(acc.id)}`,
53
+ { headers: { Authorization: `Bearer ${apiKey}` } },
54
+ );
55
+ const countData = (await countRes.json()) as {
56
+ success: boolean;
57
+ data: { count: number };
58
+ };
59
+ const count = countData.success ? countData.data.count : 0;
60
+ accountStats.push({
61
+ id: acc.id,
62
+ name: acc.name,
63
+ channelId: acc.channelId,
64
+ friendsInDb: count,
65
+ });
66
+ }
67
+
68
+ // Get health/risk level for each account
69
+ for (const acc of accountStats) {
70
+ try {
71
+ const healthRes = await fetch(
72
+ `${apiUrl}/api/accounts/${acc.id}/health`,
73
+ { headers: { Authorization: `Bearer ${apiKey}` } },
74
+ );
75
+ const healthData = (await healthRes.json()) as {
76
+ success: boolean;
77
+ data: { riskLevel: string };
78
+ };
79
+ if (healthData.success) {
80
+ acc.riskLevel = healthData.data.riskLevel;
81
+ }
82
+ } catch {
83
+ // Health endpoint may not exist yet
84
+ }
85
+ }
86
+
87
+ const [totalFriends, scenarios, broadcasts, tags, forms] =
88
+ await Promise.all([
89
+ client.friends.count(),
90
+ client.scenarios.list({ accountId }),
91
+ client.broadcasts.list({ accountId }),
92
+ client.tags.list(),
93
+ client.forms.list(),
94
+ ]);
95
+
96
+ const activeScenarios = scenarios.filter(
97
+ (s: { isActive: boolean }) => s.isActive,
98
+ );
99
+ const recentBroadcasts = broadcasts.slice(0, 5);
100
+
101
+ const summary = {
102
+ friends: {
103
+ totalDbRecords: totalFriends,
104
+ note: "totalDbRecords includes both Account \u2460 and \u2461 records. Same user on different accounts = separate records. Use per-account counts below for accurate numbers.",
105
+ perAccount: accountStats,
106
+ },
107
+ scenarios: {
108
+ total: scenarios.length,
109
+ active: activeScenarios.length,
110
+ activeList: activeScenarios.map(
111
+ (s: { id: string; name: string; triggerType: string }) => ({
112
+ id: s.id,
113
+ name: s.name,
114
+ triggerType: s.triggerType,
115
+ }),
116
+ ),
117
+ },
118
+ broadcasts: {
119
+ total: broadcasts.length,
120
+ recent: recentBroadcasts.map(
121
+ (b: {
122
+ id: string;
123
+ title: string;
124
+ status: string;
125
+ sentAt: string | null;
126
+ }) => ({
127
+ id: b.id,
128
+ title: b.title,
129
+ status: b.status,
130
+ sentAt: b.sentAt,
131
+ }),
132
+ ),
133
+ },
134
+ tags: {
135
+ total: tags.length,
136
+ list: tags.map((t: { id: string; name: string }) => ({
137
+ id: t.id,
138
+ name: t.name,
139
+ })),
140
+ },
141
+ forms: {
142
+ total: forms.length,
143
+ list: forms.map(
144
+ (f: { id: string; name: string; submitCount: number }) => ({
145
+ id: f.id,
146
+ name: f.name,
147
+ submitCount: f.submitCount,
148
+ }),
149
+ ),
150
+ },
151
+ };
152
+
153
+ return {
154
+ content: [
155
+ {
156
+ type: "text" as const,
157
+ text: JSON.stringify(summary, null, 2),
158
+ },
159
+ ],
160
+ };
161
+ } catch (error) {
162
+ return {
163
+ content: [
164
+ {
165
+ type: "text" as const,
166
+ text: JSON.stringify(
167
+ { success: false, error: String(error) },
168
+ null,
169
+ 2,
170
+ ),
171
+ },
172
+ ],
173
+ isError: true,
174
+ };
175
+ }
176
+ },
177
+ );
178
+ }
@@ -0,0 +1,195 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { getClient } from "../client.js";
4
+
5
+ export function registerBroadcast(server: McpServer): void {
6
+ server.tool(
7
+ "broadcast",
8
+ "Send a broadcast message to all friends, a specific tag group, or a filtered segment. Creates and immediately sends the broadcast.",
9
+ {
10
+ title: z
11
+ .string()
12
+ .describe("Internal title for this broadcast (not shown to users)"),
13
+ messageType: z.enum(["text", "flex"]).describe("Message type"),
14
+ messageContent: z
15
+ .string()
16
+ .describe(
17
+ "Message content. For text: plain string. For flex: JSON string.",
18
+ ),
19
+ targetType: z
20
+ .enum(["all", "tag", "segment"])
21
+ .default("all")
22
+ .describe(
23
+ "Target audience: 'all' for everyone, 'tag' for a tag group, 'segment' for filtered conditions",
24
+ ),
25
+ targetTagId: z
26
+ .string()
27
+ .optional()
28
+ .describe("Tag ID when targetType is 'tag'"),
29
+ segmentConditions: z
30
+ .string()
31
+ .optional()
32
+ .describe(
33
+ "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} }] }",
34
+ ),
35
+ scheduledAt: z
36
+ .string()
37
+ .optional()
38
+ .describe("ISO 8601 datetime to schedule. Omit to send immediately."),
39
+ accountId: z
40
+ .string()
41
+ .optional()
42
+ .describe("LINE account ID (uses default if omitted)"),
43
+ },
44
+ async ({
45
+ title,
46
+ messageType,
47
+ messageContent,
48
+ targetType,
49
+ targetTagId,
50
+ segmentConditions,
51
+ scheduledAt,
52
+ accountId,
53
+ }) => {
54
+ try {
55
+ const client = getClient();
56
+
57
+ if (targetType === "segment" && !segmentConditions) {
58
+ return {
59
+ content: [
60
+ {
61
+ type: "text" as const,
62
+ text: JSON.stringify(
63
+ {
64
+ success: false,
65
+ error:
66
+ "segmentConditions is required when targetType is 'segment'",
67
+ },
68
+ null,
69
+ 2,
70
+ ),
71
+ },
72
+ ],
73
+ isError: true,
74
+ };
75
+ }
76
+
77
+ if (targetType === "segment" && scheduledAt) {
78
+ return {
79
+ content: [
80
+ {
81
+ type: "text" as const,
82
+ text: JSON.stringify(
83
+ {
84
+ success: false,
85
+ error:
86
+ "Scheduled segment broadcasts are not supported. Use scheduledAt only with targetType 'all' or 'tag'.",
87
+ },
88
+ null,
89
+ 2,
90
+ ),
91
+ },
92
+ ],
93
+ isError: true,
94
+ };
95
+ }
96
+
97
+ if (targetType === "segment" && segmentConditions) {
98
+ let parsedConditions;
99
+ try {
100
+ parsedConditions = JSON.parse(segmentConditions);
101
+ } catch {
102
+ return {
103
+ content: [
104
+ {
105
+ type: "text" as const,
106
+ text: JSON.stringify(
107
+ {
108
+ success: false,
109
+ error: "segmentConditions must be valid JSON",
110
+ },
111
+ null,
112
+ 2,
113
+ ),
114
+ },
115
+ ],
116
+ isError: true,
117
+ };
118
+ }
119
+
120
+ const broadcast = await client.broadcasts.create({
121
+ title: `[SEGMENT] ${title}`,
122
+ messageType,
123
+ messageContent,
124
+ targetType: "all",
125
+ lineAccountId: accountId,
126
+ });
127
+
128
+ try {
129
+ const result = await client.broadcasts.sendToSegment(
130
+ broadcast.id,
131
+ parsedConditions,
132
+ );
133
+ return {
134
+ content: [
135
+ {
136
+ type: "text" as const,
137
+ text: JSON.stringify(
138
+ { success: true, broadcast: result },
139
+ null,
140
+ 2,
141
+ ),
142
+ },
143
+ ],
144
+ };
145
+ } catch (sendError) {
146
+ await client.broadcasts.delete(broadcast.id).catch(() => {});
147
+ throw sendError;
148
+ }
149
+ }
150
+
151
+ // At this point targetType is guaranteed to be 'all' or 'tag' (segment handled above)
152
+ const broadcast = await client.broadcasts.create({
153
+ title,
154
+ messageType,
155
+ messageContent,
156
+ targetType: targetType as "all" | "tag",
157
+ targetTagId,
158
+ scheduledAt,
159
+ lineAccountId: accountId,
160
+ });
161
+
162
+ const result = scheduledAt
163
+ ? broadcast
164
+ : await client.broadcasts.send(broadcast.id);
165
+
166
+ return {
167
+ content: [
168
+ {
169
+ type: "text" as const,
170
+ text: JSON.stringify(
171
+ { success: true, broadcast: result },
172
+ null,
173
+ 2,
174
+ ),
175
+ },
176
+ ],
177
+ };
178
+ } catch (error) {
179
+ return {
180
+ content: [
181
+ {
182
+ type: "text" as const,
183
+ text: JSON.stringify(
184
+ { success: false, error: String(error) },
185
+ null,
186
+ 2,
187
+ ),
188
+ },
189
+ ],
190
+ isError: true,
191
+ };
192
+ }
193
+ },
194
+ );
195
+ }
@@ -0,0 +1,80 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { getClient } from "../client.js";
4
+
5
+ export function registerCreateForm(server: McpServer): void {
6
+ server.tool(
7
+ "create_form",
8
+ "Create a form for collecting user responses. Can auto-tag responders and enroll them in scenarios.",
9
+ {
10
+ name: z.string().describe("Form name"),
11
+ description: z
12
+ .string()
13
+ .optional()
14
+ .describe("Form description shown to users"),
15
+ fields: z
16
+ .string()
17
+ .describe(
18
+ "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 }]",
19
+ ),
20
+ onSubmitTagId: z
21
+ .string()
22
+ .optional()
23
+ .describe("Tag ID to auto-apply when form is submitted"),
24
+ onSubmitScenarioId: z
25
+ .string()
26
+ .optional()
27
+ .describe("Scenario ID to auto-enroll when form is submitted"),
28
+ saveToMetadata: z
29
+ .boolean()
30
+ .default(true)
31
+ .describe("Save form responses to friend metadata"),
32
+ accountId: z
33
+ .string()
34
+ .optional()
35
+ .describe("LINE account ID (uses default if omitted)"),
36
+ },
37
+ async ({
38
+ name,
39
+ description,
40
+ fields,
41
+ onSubmitTagId,
42
+ onSubmitScenarioId,
43
+ saveToMetadata,
44
+ }) => {
45
+ try {
46
+ const client = getClient();
47
+ const form = await client.forms.create({
48
+ name,
49
+ description,
50
+ fields: JSON.parse(fields),
51
+ onSubmitTagId,
52
+ onSubmitScenarioId,
53
+ saveToMetadata,
54
+ });
55
+ return {
56
+ content: [
57
+ {
58
+ type: "text" as const,
59
+ text: JSON.stringify({ success: true, form }, null, 2),
60
+ },
61
+ ],
62
+ };
63
+ } catch (error) {
64
+ return {
65
+ content: [
66
+ {
67
+ type: "text" as const,
68
+ text: JSON.stringify(
69
+ { success: false, error: String(error) },
70
+ null,
71
+ 2,
72
+ ),
73
+ },
74
+ ],
75
+ isError: true,
76
+ };
77
+ }
78
+ },
79
+ );
80
+ }