Skip to content

Commit 25ce7f8

Browse files
committed
Added FM Delivery Data API MCP Tool
1 parent 78e6ec8 commit 25ce7f8

File tree

5 files changed

+222
-1
lines changed

5 files changed

+222
-1
lines changed

src/api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ export const remoteConfigApiOrigin = () =>
125125
utils.envOverride("FIREBASE_REMOTE_CONFIG_URL", "https://firebaseremoteconfig.googleapis.com");
126126
export const messagingApiOrigin = () =>
127127
utils.envOverride("FIREBASE_MESSAGING_CONFIG_URL", "https://fcm.googleapis.com");
128+
export const messagingDataApiOrigin = () =>
129+
utils.envOverride("FIREBASE_MESSAGING_DATA_CONFIG_URL", "https://content-fcmdata.googleapis.com");
128130
export const crashlyticsApiOrigin = () =>
129131
utils.envOverride("FIREBASE_CRASHLYTICS_URL", "https://firebasecrashlytics.googleapis.com");
130132
export const resourceManagerOrigin = () =>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { z } from "zod";
2+
import { tool } from "../../tool";
3+
import { mcpError, toContent } from "../../util";
4+
import { getAndroidDeliveryData } from "../../../messaging/getDeliveryData";
5+
6+
export const get_fcm_delivery_data = tool(
7+
"messaging",
8+
{
9+
name: "get_fcm_delivery_data",
10+
description: "Gets FCM's delivery data",
11+
inputSchema: z.object({
12+
appId: z.string().describe("appId to fetch data for"),
13+
pageSize: z.number().optional().describe("How many results to fetch"),
14+
pageToken: z.string().optional().describe("Next page token"),
15+
}),
16+
annotations: {
17+
title: "Fetch FCM Delivery Data",
18+
},
19+
_meta: {
20+
requiresAuth: true,
21+
requiresProject: true,
22+
},
23+
},
24+
async ({ appId, pageSize, pageToken }, { projectId }) => {
25+
if (!appId.includes(":android:")) {
26+
return mcpError(
27+
`Invalid app id provided: ${appId}. Currently fcm delivery data is only available for android apps.`,
28+
);
29+
}
30+
31+
return toContent(await getAndroidDeliveryData(projectId, appId, { pageSize, pageToken }));
32+
},
33+
);

src/mcp/tools/messaging/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ServerTool } from "../../tool";
22
import { send_message } from "./send_message";
3+
import { get_fcm_delivery_data } from "./get_delivery_data";
34

4-
export const messagingTools: ServerTool[] = [send_message];
5+
export const messagingTools: ServerTool[] = [send_message, get_fcm_delivery_data];

src/messaging/getDeliveryData.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { messagingDataApiOrigin } from "../api";
2+
import { Client } from "../apiv2";
3+
import { logger } from "../logger";
4+
import { FirebaseError } from "../error";
5+
import { ListAndroidDeliveryDataResponse } from "./interfaces";
6+
7+
const TIMEOUT = 10000;
8+
9+
const apiClient = new Client({
10+
urlPrefix: messagingDataApiOrigin(),
11+
apiVersion: "v1beta1",
12+
});
13+
14+
export async function getAndroidDeliveryData(
15+
projectId: string,
16+
androidAppId: string,
17+
options: {
18+
pageSize?: number;
19+
pageToken?: string;
20+
},
21+
): Promise<ListAndroidDeliveryDataResponse> {
22+
try {
23+
// API docs for fetching Android delivery data are here:
24+
// https://firebase.google.com/docs/reference/fcmdata/rest/v1beta1/projects.androidApps.deliveryData/list#http-request
25+
26+
const customHeaders = {
27+
"Content-Type": "application/json",
28+
"x-goog-user-project": projectId,
29+
};
30+
31+
// set up query params
32+
const params = new URLSearchParams();
33+
if (options.pageSize) {
34+
params.set("pageSize", String(options.pageSize));
35+
}
36+
if (options.pageToken) {
37+
params.set("pageToken", options.pageToken);
38+
}
39+
40+
logger.debug(`requesting android delivery data for ${projectId}, ${androidAppId}`);
41+
42+
const res = await apiClient.request<null, ListAndroidDeliveryDataResponse>({
43+
method: "GET",
44+
path: `/projects/${projectId}/androidApps/${androidAppId}/deliveryData`,
45+
queryParams: params,
46+
headers: customHeaders,
47+
timeout: TIMEOUT,
48+
});
49+
50+
logger.debug(`${res.status}, ${res.response}, ${res.body}`);
51+
return res.body;
52+
} catch (err: any) {
53+
logger.debug(err.message);
54+
throw new FirebaseError(
55+
`Failed to fetch delivery data for project ${projectId} and ${androidAppId}, ${err}.`,
56+
{ original: err },
57+
);
58+
}
59+
}

src/messaging/interfaces.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,129 @@ export interface Notification {
3131
/** URL of an image to include in the notification. */
3232
image?: string;
3333
}
34+
35+
// -----------------------------------------------------------------------------
36+
// FM Delivery Data Interfaces
37+
// -----------------------------------------------------------------------------
38+
39+
/**
40+
* Additional information about [proxy notification] delivery.
41+
* All percentages are calculated with 'countNotificationsAccepted' as the denominator.
42+
*/
43+
export interface ProxyNotificationInsightPercents {
44+
/** The percentage of accepted notifications that were successfully proxied. */
45+
proxied?: number;
46+
/** The percentage of accepted notifications that failed to be proxied. */
47+
failed?: number;
48+
/** The percentage of accepted notifications that were skipped because proxy notification is unsupported for the recipient. */
49+
skippedUnsupported: number;
50+
/** The percentage of accepted notifications that were skipped because the messages were not throttled. */
51+
skippedNotThrottled: number;
52+
/** The percentage of accepted notifications that were skipped because configurations required for notifications to be proxied were missing. */
53+
skippedUnconfigured: number;
54+
/** The percentage of accepted notifications that were skipped because the app disallowed these messages to be proxied. */
55+
skippedOptedOut: number;
56+
}
57+
58+
/**
59+
* Additional information about message delivery. All percentages are calculated
60+
* with 'countMessagesAccepted' as the denominator.
61+
*/
62+
export interface MessageInsightPercents {
63+
/** The percentage of accepted messages that had their priority lowered from high to normal. */
64+
priorityLowered: number;
65+
}
66+
67+
/**
68+
* Overview of delivery performance for messages that were successfully delivered.
69+
* All percentages are calculated with 'countMessagesAccepted' as the denominator.
70+
*/
71+
export interface DeliveryPerformancePercents {
72+
/** The percentage of accepted messages that were delivered to the device without delay from the FCM system. */
73+
deliveredNoDelay: number;
74+
/** The percentage of accepted messages that were delayed because the target device was not connected at the time of sending. */
75+
delayedDeviceOffline: number;
76+
/** The percentage of accepted messages that were delayed because the device was in doze mode. */
77+
delayedDeviceDoze: number;
78+
/** The percentage of accepted messages that were delayed due to message throttling. */
79+
delayedMessageThrottled: number;
80+
/** The percentage of accepted messages that were delayed because the intended device user-profile was stopped. */
81+
delayedUserStopped: number;
82+
}
83+
84+
/**
85+
* Percentage breakdown of message delivery outcomes. These categories are mutually exclusive.
86+
* All percentages are calculated with 'countMessagesAccepted' as the denominator.
87+
*/
88+
export interface MessageOutcomePercents {
89+
/** The percentage of all accepted messages that were successfully delivered to the device. */
90+
delivered: number;
91+
/** The percentage of messages accepted that were not dropped and not delivered, due to the device being disconnected. */
92+
pending: number;
93+
/** The percentage of accepted messages that were collapsed by another message. */
94+
collapsed: number;
95+
/** The percentage of accepted messages that were dropped due to too many undelivered non-collapsible messages. */
96+
droppedTooManyPendingMessages: number;
97+
/** The percentage of accepted messages that were dropped because the application was force stopped. */
98+
droppedAppForceStopped: number;
99+
/** The percentage of accepted messages that were dropped because the target device is inactive. */
100+
droppedDeviceInactive: number;
101+
/** The percentage of accepted messages that expired because Time To Live (TTL) elapsed. */
102+
droppedTtlExpired: number;
103+
}
104+
105+
/**
106+
* Data detailing messaging delivery
107+
*/
108+
export interface Data {
109+
/** Count of messages accepted by FCM intended for Android devices. */
110+
countMessagesAccepted: string; // Use string for int64 to prevent potential precision issues
111+
/** Count of notifications accepted by FCM intended for Android devices. */
112+
countNotificationsAccepted: string; // Use string for int64
113+
/** Mutually exclusive breakdown of message delivery outcomes. */
114+
messageOutcomePercents: MessageOutcomePercents;
115+
/** Additional information about delivery performance for messages that were successfully delivered. */
116+
deliveryPerformancePercents: DeliveryPerformancePercents;
117+
/** Additional general insights about message delivery. */
118+
messageInsightPercents: MessageInsightPercents;
119+
/** Additional insights about proxy notification delivery. */
120+
proxyNotificationInsightPercents: ProxyNotificationInsightPercents;
121+
}
122+
123+
// -----------------------------------------------------------------------------
124+
// Core API Interfaces
125+
// -----------------------------------------------------------------------------
126+
127+
/**
128+
* Message delivery data for a given date, app, and analytics label combination.
129+
*/
130+
export interface AndroidDeliveryData {
131+
/** The app ID to which the messages were sent. */
132+
appId: string;
133+
/** The date represented by this entry. */
134+
date: {
135+
year: number;
136+
month: number;
137+
day: number;
138+
};
139+
/** The analytics label associated with the messages sent. */
140+
analyticsLabel: string;
141+
/** The data for the specified combination. */
142+
data: Data;
143+
}
144+
145+
/**
146+
* Response message for ListAndroidDeliveryData.
147+
*/
148+
export interface ListAndroidDeliveryDataResponse {
149+
/**
150+
* The delivery data for the provided app.
151+
* There will be one entry per combination of app, date, and analytics label.
152+
*/
153+
androidDeliveryData: AndroidDeliveryData[];
154+
/**
155+
* A token, which can be sent as `page_token` to retrieve the next page.
156+
* If this field is omitted, there are no subsequent pages.
157+
*/
158+
nextPageToken?: string;
159+
}

0 commit comments

Comments
 (0)