Skip to content

Commit 15ff15e

Browse files
committed
Added FM Delivery Data API MCP Tool
1 parent 78e6ec8 commit 15ff15e

File tree

5 files changed

+227
-1
lines changed

5 files changed

+227
-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: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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
14+
.number()
15+
.optional()
16+
.describe("How many results to fetch",),
17+
pageToken: z
18+
.string()
19+
.optional()
20+
.describe("Next page token"),
21+
}),
22+
annotations: {
23+
title: "Fetch FCM Delivery Data",
24+
},
25+
_meta: {
26+
requiresAuth: true,
27+
requiresProject: true,
28+
},
29+
},
30+
async ({ appId, pageSize, pageToken }, { projectId }) => {
31+
if (!appId.includes(":android:")) {
32+
return mcpError(`Invalid app id provided: ${appId}. Currently fcm delivery data is only available for android apps.`)
33+
}
34+
35+
return toContent(
36+
await getAndroidDeliveryData(projectId, appId, { pageSize, pageToken }),
37+
);
38+
},
39+
);

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

src/messaging/interfaces.ts

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

0 commit comments

Comments
 (0)