diff --git a/src/api.ts b/src/api.ts index c7118000dde..f1b89a0f322 100755 --- a/src/api.ts +++ b/src/api.ts @@ -125,6 +125,8 @@ export const remoteConfigApiOrigin = () => utils.envOverride("FIREBASE_REMOTE_CONFIG_URL", "https://firebaseremoteconfig.googleapis.com"); export const messagingApiOrigin = () => utils.envOverride("FIREBASE_MESSAGING_CONFIG_URL", "https://fcm.googleapis.com"); +export const messagingDataApiOrigin = () => + utils.envOverride("FIREBASE_MESSAGING_DATA_CONFIG_URL", "https://content-fcmdata.googleapis.com"); export const crashlyticsApiOrigin = () => utils.envOverride("FIREBASE_CRASHLYTICS_URL", "https://firebasecrashlytics.googleapis.com"); export const resourceManagerOrigin = () => diff --git a/src/mcp/tools/messaging/get_delivery_data.ts b/src/mcp/tools/messaging/get_delivery_data.ts new file mode 100644 index 00000000000..0f50b8b6133 --- /dev/null +++ b/src/mcp/tools/messaging/get_delivery_data.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; +import { tool } from "../../tool"; +import { mcpError, toContent } from "../../util"; +import { getAndroidDeliveryData } from "../../../messaging/getDeliveryData"; + +export const get_fcm_delivery_data = tool( + "messaging", + { + name: "get_fcm_delivery_data", + description: "Gets FCM's delivery data", + inputSchema: z.object({ + appId: z.string().describe("appId to fetch data for"), + pageSize: z.number().optional().describe("How many results to fetch"), + pageToken: z.string().optional().describe("Next page token"), + }), + annotations: { + title: "Fetch FCM Delivery Data", + }, + _meta: { + requiresAuth: true, + requiresProject: true, + }, + }, + async ({ appId, pageSize, pageToken }, { projectId }) => { + if (!appId.includes(":android:")) { + return mcpError( + `Invalid app id provided: ${appId}. Currently fcm delivery data is only available for android apps.`, + ); + } + + return toContent(await getAndroidDeliveryData(projectId, appId, { pageSize, pageToken })); + }, +); diff --git a/src/mcp/tools/messaging/index.ts b/src/mcp/tools/messaging/index.ts index 409e76fff7d..475f8e7daaf 100644 --- a/src/mcp/tools/messaging/index.ts +++ b/src/mcp/tools/messaging/index.ts @@ -1,4 +1,5 @@ import { ServerTool } from "../../tool"; import { send_message } from "./send_message"; +import { get_fcm_delivery_data } from "./get_delivery_data"; -export const messagingTools: ServerTool[] = [send_message]; +export const messagingTools: ServerTool[] = [send_message, get_fcm_delivery_data]; diff --git a/src/messaging/getDeliveryData.ts b/src/messaging/getDeliveryData.ts new file mode 100644 index 00000000000..07cfd05f389 --- /dev/null +++ b/src/messaging/getDeliveryData.ts @@ -0,0 +1,59 @@ +import { messagingDataApiOrigin } from "../api"; +import { Client } from "../apiv2"; +import { logger } from "../logger"; +import { FirebaseError } from "../error"; +import { ListAndroidDeliveryDataResponse } from "./interfaces"; + +const TIMEOUT = 10000; + +const apiClient = new Client({ + urlPrefix: messagingDataApiOrigin(), + apiVersion: "v1beta1", +}); + +export async function getAndroidDeliveryData( + projectId: string, + androidAppId: string, + options: { + pageSize?: number; + pageToken?: string; + }, +): Promise { + try { + // API docs for fetching Android delivery data are here: + // https://firebase.google.com/docs/reference/fcmdata/rest/v1beta1/projects.androidApps.deliveryData/list#http-request + + const customHeaders = { + "Content-Type": "application/json", + "x-goog-user-project": projectId, + }; + + // set up query params + const params = new URLSearchParams(); + if (options.pageSize) { + params.set("pageSize", String(options.pageSize)); + } + if (options.pageToken) { + params.set("pageToken", options.pageToken); + } + + logger.debug(`requesting android delivery data for ${projectId}, ${androidAppId}`); + + const res = await apiClient.request({ + method: "GET", + path: `/projects/${projectId}/androidApps/${androidAppId}/deliveryData`, + queryParams: params, + headers: customHeaders, + timeout: TIMEOUT, + }); + + logger.debug(`${res.status}, ${res.response}, ${res.body}`); + return res.body; + } catch (err: any) { + logger.debug(err.message); + throw new FirebaseError( + `Failed to fetch delivery data for project ${projectId} and ${androidAppId}, ${err}.`, + { original: err }, + ); + } +} diff --git a/src/messaging/interfaces.ts b/src/messaging/interfaces.ts index f63a05dc264..8ba22c8696c 100644 --- a/src/messaging/interfaces.ts +++ b/src/messaging/interfaces.ts @@ -31,3 +31,129 @@ export interface Notification { /** URL of an image to include in the notification. */ image?: string; } + +// ----------------------------------------------------------------------------- +// FM Delivery Data Interfaces +// ----------------------------------------------------------------------------- + +/** + * Additional information about [proxy notification] delivery. + * All percentages are calculated with 'countNotificationsAccepted' as the denominator. + */ +export interface ProxyNotificationInsightPercents { + /** The percentage of accepted notifications that were successfully proxied. */ + proxied?: number; + /** The percentage of accepted notifications that failed to be proxied. */ + failed?: number; + /** The percentage of accepted notifications that were skipped because proxy notification is unsupported for the recipient. */ + skippedUnsupported: number; + /** The percentage of accepted notifications that were skipped because the messages were not throttled. */ + skippedNotThrottled: number; + /** The percentage of accepted notifications that were skipped because configurations required for notifications to be proxied were missing. */ + skippedUnconfigured: number; + /** The percentage of accepted notifications that were skipped because the app disallowed these messages to be proxied. */ + skippedOptedOut: number; +} + +/** + * Additional information about message delivery. All percentages are calculated + * with 'countMessagesAccepted' as the denominator. + */ +export interface MessageInsightPercents { + /** The percentage of accepted messages that had their priority lowered from high to normal. */ + priorityLowered: number; +} + +/** + * Overview of delivery performance for messages that were successfully delivered. + * All percentages are calculated with 'countMessagesAccepted' as the denominator. + */ +export interface DeliveryPerformancePercents { + /** The percentage of accepted messages that were delivered to the device without delay from the FCM system. */ + deliveredNoDelay: number; + /** The percentage of accepted messages that were delayed because the target device was not connected at the time of sending. */ + delayedDeviceOffline: number; + /** The percentage of accepted messages that were delayed because the device was in doze mode. */ + delayedDeviceDoze: number; + /** The percentage of accepted messages that were delayed due to message throttling. */ + delayedMessageThrottled: number; + /** The percentage of accepted messages that were delayed because the intended device user-profile was stopped. */ + delayedUserStopped: number; +} + +/** + * Percentage breakdown of message delivery outcomes. These categories are mutually exclusive. + * All percentages are calculated with 'countMessagesAccepted' as the denominator. + */ +export interface MessageOutcomePercents { + /** The percentage of all accepted messages that were successfully delivered to the device. */ + delivered: number; + /** The percentage of messages accepted that were not dropped and not delivered, due to the device being disconnected. */ + pending: number; + /** The percentage of accepted messages that were collapsed by another message. */ + collapsed: number; + /** The percentage of accepted messages that were dropped due to too many undelivered non-collapsible messages. */ + droppedTooManyPendingMessages: number; + /** The percentage of accepted messages that were dropped because the application was force stopped. */ + droppedAppForceStopped: number; + /** The percentage of accepted messages that were dropped because the target device is inactive. */ + droppedDeviceInactive: number; + /** The percentage of accepted messages that expired because Time To Live (TTL) elapsed. */ + droppedTtlExpired: number; +} + +/** + * Data detailing messaging delivery + */ +export interface Data { + /** Count of messages accepted by FCM intended for Android devices. */ + countMessagesAccepted: string; // Use string for int64 to prevent potential precision issues + /** Count of notifications accepted by FCM intended for Android devices. */ + countNotificationsAccepted: string; // Use string for int64 + /** Mutually exclusive breakdown of message delivery outcomes. */ + messageOutcomePercents: MessageOutcomePercents; + /** Additional information about delivery performance for messages that were successfully delivered. */ + deliveryPerformancePercents: DeliveryPerformancePercents; + /** Additional general insights about message delivery. */ + messageInsightPercents: MessageInsightPercents; + /** Additional insights about proxy notification delivery. */ + proxyNotificationInsightPercents: ProxyNotificationInsightPercents; +} + +// ----------------------------------------------------------------------------- +// Core API Interfaces +// ----------------------------------------------------------------------------- + +/** + * Message delivery data for a given date, app, and analytics label combination. + */ +export interface AndroidDeliveryData { + /** The app ID to which the messages were sent. */ + appId: string; + /** The date represented by this entry. */ + date: { + year: number; + month: number; + day: number; + }; + /** The analytics label associated with the messages sent. */ + analyticsLabel: string; + /** The data for the specified combination. */ + data: Data; +} + +/** + * Response message for ListAndroidDeliveryData. + */ +export interface ListAndroidDeliveryDataResponse { + /** + * The delivery data for the provided app. + * There will be one entry per combination of app, date, and analytics label. + */ + androidDeliveryData: AndroidDeliveryData[]; + /** + * A token, which can be sent as `page_token` to retrieve the next page. + * If this field is omitted, there are no subsequent pages. + */ + nextPageToken?: string; +}