Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ LINE公式アカウントとAI Agentを接続するために、LINE Messaging AP
- `start` (string?): 次のユーザーID配列を取得するための継続トークン。前回のレスポンスの`next`プロパティから取得できる。
- `limit` (number?): 1回のリクエストで取得するユーザーIDの最大数。

13. **get_rich_menu_insight_summary**
- 指定した期間のリッチメニューの統計情報のサマリーを、期間全体で集計した1つの結果として取得する。
- **入力:**
- `richMenuId` (string): Messaging APIで作成したリッチメニューのID。
- `from` (string): 集計期間の開始日(この日を含む)。yyyyMMdd形式。直近3年以内である必要がある。
- `to` (string): 集計期間の終了日(この日を含む)。yyyyMMdd形式。開始日から最大396日後まで指定できる。

14. **get_rich_menu_insight_daily**
- 指定した期間のリッチメニューの統計情報を日別に取得する。
- **入力:**
- `richMenuId` (string): Messaging APIで作成したリッチメニューのID。
- `from` (string): 集計期間の開始日(この日を含む)。yyyyMMdd形式。直近3年以内である必要がある。
- `to` (string): 集計期間の終了日(この日を含む)。yyyyMMdd形式。開始日から最大99日後まで指定できる。

## インストール (npxを使用)

要件:
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@
- `start` (string?): Continuation token to get the next array of user IDs. Returned in the `next` property of a previous response.
- `limit` (number?): The maximum number of user IDs to retrieve in a single request.

13. **get_rich_menu_insight_summary**
- Get a summary of rich menu statistics for the specified period, aggregated over the entire period as a single result.
- **Inputs:**
- `richMenuId` (string): ID of the rich menu created via the Messaging API.
- `from` (string): Start date of the aggregation period (inclusive), in yyyyMMdd format. Must be within the most recent 3 years.
- `to` (string): End date of the aggregation period (inclusive), in yyyyMMdd format. Up to 396 days after the start date.

14. **get_rich_menu_insight_daily**
- Get rich menu statistics broken down by day for the specified period.
- **Inputs:**
- `richMenuId` (string): ID of the rich menu created via the Messaging API.
- `from` (string): Start date of the aggregation period (inclusive), in yyyyMMdd format. Must be within the most recent 3 years.
- `to` (string): End date of the aggregation period (inclusive), in yyyyMMdd format. Up to 99 days after the start date.

## Installation (Using npx)

requirements:
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"homepage": "https://github.com/line/line-bot-mcp-server",
"bugs": "https://github.com/line/line-bot-mcp-server/issues",
"dependencies": {
"@line/bot-sdk": "^11.0.1",
"@line/bot-sdk": "^11.1.0",
"@marp-team/marp-core": "^4.1.0",
"@modelcontextprotocol/sdk": "^1.8.0",
"puppeteer": "^25.0.0",
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import DeleteRichMenu from "./tools/deleteRichMenu.js";
import SetRichMenuDefault from "./tools/setRichMenuDefault.js";
import CreateRichMenu from "./tools/createRichMenu.js";
import GetFollowerIds from "./tools/getFollowerIds.js";
import GetRichMenuInsightSummary from "./tools/getRichMenuInsightSummary.js";
import GetRichMenuInsightDaily from "./tools/getRichMenuInsightDaily.js";

const server = new McpServer({
name: "line-bot",
Expand Down Expand Up @@ -62,6 +64,8 @@ new SetRichMenuDefault(lineBotClient).register(server);
new CancelRichMenuDefault(lineBotClient).register(server);
new CreateRichMenu(lineBotClient).register(server);
new GetFollowerIds(lineBotClient).register(server);
new GetRichMenuInsightSummary(lineBotClient).register(server);
new GetRichMenuInsightDaily(lineBotClient).register(server);

async function main() {
if (!process.env.CHANNEL_ACCESS_TOKEN) {
Expand Down
67 changes: 67 additions & 0 deletions src/tools/getRichMenuInsightDaily.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { LineBotClient } from "@line/bot-sdk";
import {
createErrorResponse,
createSuccessResponse,
} from "../common/response.js";
import { AbstractTool } from "./AbstractTool.js";
import { z } from "zod";

export default class GetRichMenuInsightDaily extends AbstractTool {
private client: LineBotClient;

constructor(client: LineBotClient) {
super();
this.client = client;
}

register(server: McpServer) {
server.registerTool(
"get_rich_menu_insight_daily",
{
title: "Get Rich Menu Insight Daily",
description:
"Get rich menu statistics broken down by day for the specified period, for a rich menu created via the Messaging API. " +
"Returns the daily impression count for the whole rich menu and the daily click count for each tappable area. " +
"When the total number of unique clicks during the period is below the privacy threshold, " +
"only richMenuId is returned and the other fields are omitted.",
inputSchema: {
richMenuId: z
.string()
.describe("ID of the rich menu created via the Messaging API."),
from: z
.string()
.regex(/^[0-9]{8}$/)
.describe(
"Start date of the aggregation period (inclusive). Must be within the most recent 3 years. " +
"Format: yyyyMMdd (e.g. 20260213). Time zone: UTC+9",
),
to: z
.string()
.regex(/^[0-9]{8}$/)
.describe(
"End date of the aggregation period (inclusive). The end date can be specified for up to 99 days after the start date. " +
"Format: yyyyMMdd (e.g. 20260215). Time zone: UTC+9",
),
},
annotations: {
readOnlyHint: true,
},
},
async ({ richMenuId, from, to }) => {
try {
const response = await this.client.getRichMenuInsightDaily(
richMenuId,
from,
to,
);
return createSuccessResponse(response);
} catch (error: unknown) {
return createErrorResponse(
`Failed to get rich menu insight daily: ${error instanceof Error ? error.message : String(error)}`,
);
}
},
);
}
}
68 changes: 68 additions & 0 deletions src/tools/getRichMenuInsightSummary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { LineBotClient } from "@line/bot-sdk";
import {
createErrorResponse,
createSuccessResponse,
} from "../common/response.js";
import { AbstractTool } from "./AbstractTool.js";
import { z } from "zod";

export default class GetRichMenuInsightSummary extends AbstractTool {
private client: LineBotClient;

constructor(client: LineBotClient) {
super();
this.client = client;
}

register(server: McpServer) {
server.registerTool(
"get_rich_menu_insight_summary",
{
title: "Get Rich Menu Insight Summary",
description:
"Get a summary of rich menu statistics for the specified period, for a rich menu created via the Messaging API. " +
"Returns the total impression count for the whole rich menu and the click count for each tappable area, " +
"aggregated over the entire period as a single result. " +
"When the total number of unique clicks during the period is below the privacy threshold, " +
"only richMenuId is returned and the other fields are omitted.",
inputSchema: {
richMenuId: z
.string()
.describe("ID of the rich menu created via the Messaging API."),
from: z
.string()
.regex(/^[0-9]{8}$/)
.describe(
"Start date of the aggregation period (inclusive). Must be within the most recent 3 years. " +
"Format: yyyyMMdd (e.g. 20260213). Time zone: UTC+9",
),
to: z
.string()
.regex(/^[0-9]{8}$/)
.describe(
"End date of the aggregation period (inclusive). The end date can be specified for up to 396 days after the start date. " +
"Format: yyyyMMdd (e.g. 20260215). Time zone: UTC+9",
),
},
annotations: {
readOnlyHint: true,
},
},
async ({ richMenuId, from, to }) => {
try {
const response = await this.client.getRichMenuInsightSummary(
richMenuId,
from,
to,
);
return createSuccessResponse(response);
} catch (error: unknown) {
return createErrorResponse(
`Failed to get rich menu insight summary: ${error instanceof Error ? error.message : String(error)}`,
);
}
},
);
}
}
2 changes: 2 additions & 0 deletions test/helpers/mock-line-clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ export function createMockLineBotClient() {
createRichMenu: vi.fn(),
getFollowers: vi.fn(),
setRichMenuImage: vi.fn(),
getRichMenuInsightSummary: vi.fn(),
getRichMenuInsightDaily: vi.fn(),
} as unknown as LineBotClient;
}
89 changes: 89 additions & 0 deletions test/tools/getRichMenuInsightDaily.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { createMockLineBotClient } from "../helpers/mock-line-clients.js";
import GetRichMenuInsightDaily from "../../src/tools/getRichMenuInsightDaily.js";

describe("get_rich_menu_insight_daily tool", () => {
let client: Client;
let server: McpServer;
let mockLineClient: ReturnType<typeof createMockLineBotClient>;

beforeEach(async () => {
mockLineClient = createMockLineBotClient();
server = new McpServer({ name: "test", version: "0.0.1" });
new GetRichMenuInsightDaily(mockLineClient).register(server);

const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
client = new Client({ name: "test-client", version: "0.0.1" });
await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport),
]);
});

afterEach(async () => {
await client?.close();
await server?.close();
});

it("calls getRichMenuInsightDaily with the correct arguments", async () => {
vi.mocked(mockLineClient.getRichMenuInsightDaily).mockResolvedValue({
richMenuId: "richmenu-123",
metricsFrom: "20260213",
metricsTo: "20260215",
} as never);

const result = await client.callTool({
name: "get_rich_menu_insight_daily",
arguments: {
richMenuId: "richmenu-123",
from: "20260213",
to: "20260215",
},
});

expect(mockLineClient.getRichMenuInsightDaily).toHaveBeenCalledWith(
"richmenu-123",
"20260213",
"20260215",
);
expect(result.isError).toBeFalsy();
});

it("returns an error response when LINE API fails", async () => {
vi.mocked(mockLineClient.getRichMenuInsightDaily).mockRejectedValue(
new Error("Not found"),
);

const result = await client.callTool({
name: "get_rich_menu_insight_daily",
arguments: {
richMenuId: "richmenu-unknown",
from: "20260213",
to: "20260215",
},
});

expect(result.isError).toBe(true);
const text = (result.content as Array<{ type: string; text: string }>)[0]
.text;
expect(text).toContain("Failed to get rich menu insight daily");
});

it("rejects an invalid date format", async () => {
const result = await client.callTool({
name: "get_rich_menu_insight_daily",
arguments: {
richMenuId: "richmenu-123",
from: "2026-02-13",
to: "20260215",
},
});

expect(result.isError).toBe(true);
expect(mockLineClient.getRichMenuInsightDaily).not.toHaveBeenCalled();
});
});
Loading