diff --git a/README.ja.md b/README.ja.md index a37dffd3..72a60a0d 100644 --- a/README.ja.md +++ b/README.ja.md @@ -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を使用) 要件: diff --git a/README.md b/README.md index d4112e52..32a35a41 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/package-lock.json b/package-lock.json index d5f6f302..bd6e65ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.1-local", "license": "Apache-2.0", "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", @@ -153,9 +153,9 @@ "license": "MIT" }, "node_modules/@line/bot-sdk": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/@line/bot-sdk/-/bot-sdk-11.0.2.tgz", - "integrity": "sha512-Y2PCEIlKw5ytu56HhrZt0Wfb8ZxBjrpKTlsmaZ4nWkbHKV2F0T7ZpwFfT7FqXryyqOrl8+RdvFYBBXE4B464SA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@line/bot-sdk/-/bot-sdk-11.1.0.tgz", + "integrity": "sha512-i8EQziuuvNitMrqSHQfzmjiyz9CBA7KjhmAJhCWhZzbI0kElfbaOOVhxEYxJGul5Aar9dv6HrHUgPeDLNIghEQ==", "license": "Apache-2.0", "dependencies": { "@types/node": "^24.0.0" diff --git a/package.json b/package.json index 6f87f0a4..dc0676d6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/index.ts b/src/index.ts index 4e2dde25..2d59d7f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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", @@ -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) { diff --git a/src/tools/getRichMenuInsightDaily.ts b/src/tools/getRichMenuInsightDaily.ts new file mode 100644 index 00000000..726c0d0c --- /dev/null +++ b/src/tools/getRichMenuInsightDaily.ts @@ -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)}`, + ); + } + }, + ); + } +} diff --git a/src/tools/getRichMenuInsightSummary.ts b/src/tools/getRichMenuInsightSummary.ts new file mode 100644 index 00000000..f1fbf3b8 --- /dev/null +++ b/src/tools/getRichMenuInsightSummary.ts @@ -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)}`, + ); + } + }, + ); + } +} diff --git a/test/helpers/mock-line-clients.ts b/test/helpers/mock-line-clients.ts index 28113b6c..4fefe964 100644 --- a/test/helpers/mock-line-clients.ts +++ b/test/helpers/mock-line-clients.ts @@ -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; } diff --git a/test/tools/getRichMenuInsightDaily.test.ts b/test/tools/getRichMenuInsightDaily.test.ts new file mode 100644 index 00000000..26998e01 --- /dev/null +++ b/test/tools/getRichMenuInsightDaily.test.ts @@ -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; + + 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(); + }); +}); diff --git a/test/tools/getRichMenuInsightSummary.test.ts b/test/tools/getRichMenuInsightSummary.test.ts new file mode 100644 index 00000000..5504da73 --- /dev/null +++ b/test/tools/getRichMenuInsightSummary.test.ts @@ -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 GetRichMenuInsightSummary from "../../src/tools/getRichMenuInsightSummary.js"; + +describe("get_rich_menu_insight_summary tool", () => { + let client: Client; + let server: McpServer; + let mockLineClient: ReturnType; + + beforeEach(async () => { + mockLineClient = createMockLineBotClient(); + server = new McpServer({ name: "test", version: "0.0.1" }); + new GetRichMenuInsightSummary(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 getRichMenuInsightSummary with the correct arguments", async () => { + vi.mocked(mockLineClient.getRichMenuInsightSummary).mockResolvedValue({ + richMenuId: "richmenu-123", + metricsFrom: "20260213", + metricsTo: "20260215", + } as never); + + const result = await client.callTool({ + name: "get_rich_menu_insight_summary", + arguments: { + richMenuId: "richmenu-123", + from: "20260213", + to: "20260215", + }, + }); + + expect(mockLineClient.getRichMenuInsightSummary).toHaveBeenCalledWith( + "richmenu-123", + "20260213", + "20260215", + ); + expect(result.isError).toBeFalsy(); + }); + + it("returns an error response when LINE API fails", async () => { + vi.mocked(mockLineClient.getRichMenuInsightSummary).mockRejectedValue( + new Error("Not found"), + ); + + const result = await client.callTool({ + name: "get_rich_menu_insight_summary", + 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 summary"); + }); + + it("rejects an invalid date format", async () => { + const result = await client.callTool({ + name: "get_rich_menu_insight_summary", + arguments: { + richMenuId: "richmenu-123", + from: "2026-02-13", + to: "20260215", + }, + }); + + expect(result.isError).toBe(true); + expect(mockLineClient.getRichMenuInsightSummary).not.toHaveBeenCalled(); + }); +});