Skip to content

Commit bca398a

Browse files
committed
Added FM Delivery Data API MCP Tool
1 parent dd7c567 commit bca398a

File tree

5 files changed

+239
-1
lines changed

5 files changed

+239
-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: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { z } from "zod";
2+
import { tool } from "../../tool";
3+
import { mcpError, toContent } from "../../util";
4+
import { getAndroidDeliveryData } from "../../../messaging/getDeliveryData";
5+
import { FirebaseError } from "../../../error";
6+
import { logger } from "../../../logger";
7+
8+
export const get_fcm_delivery_data = tool(
9+
{
10+
name: "get_fcm_delivery_data",
11+
description:
12+
"Gets FCM's delivery data",
13+
inputSchema: z.object({
14+
appId: z.string().describe("appId to fetch data for"),
15+
pageSize: z
16+
.number()
17+
.optional()
18+
.describe(
19+
"How many results to fetch.",
20+
),
21+
pageToken: z
22+
.string()
23+
.optional()
24+
.describe("page next token."),
25+
}),
26+
annotations: {
27+
title: "Fetch FCM Delivery Data",
28+
},
29+
_meta: {
30+
requiresAuth: true,
31+
requiresProject: true,
32+
},
33+
},
34+
async ({ appId, pageSize, pageToken }, { projectId }) => {
35+
if (!appId.includes(":android:")) {
36+
throw new FirebaseError(`Invalid app id provided: ${appId}. Currently fcm delivery data is only available for android apps.`)
37+
}
38+
39+
return toContent(await getAndroidDeliveryData(projectId, appId, { pageSize, pageToken }))
40+
},
41+
);

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: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { messagingDataApiOrigin } from "../api";
2+
import { Client } from "../apiv2";
3+
import { logger } from "../logger";
4+
import { FirebaseError } from "../error";
5+
import { ListAndroidDeliveryDataResponse, ListAndroidDeliveryDataRequest } 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+
const res = await apiClient.request<null, ListAndroidDeliveryDataResponse>({
42+
method: "GET",
43+
path: `/projects/${projectId}/androidApps/${androidAppId}/deliveryData`,
44+
headers: customHeaders,
45+
timeout: TIMEOUT,
46+
});
47+
48+
logger.debug(`${res.status}, ${res.response}, ${res.body}`);
49+
return res.body;
50+
} catch (err: any) {
51+
logger.debug(err.message);
52+
throw new FirebaseError(`Failed to fetch delivery data for project ${projectId} and ${androidAppId}, ${err}.`, { original: err });
53+
}
54+
}

src/messaging/interfaces.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,143 @@ 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+
// TODO: where to get the date type from
136+
// date: string;
137+
/** The analytics label associated with the messages sent. */
138+
analyticsLabel: string;
139+
/** The data for the specified combination. */
140+
data: Data;
141+
}
142+
143+
/**
144+
* Request message for ListAndroidDeliveryData.
145+
*/
146+
export interface ListAndroidDeliveryDataRequest {
147+
/**
148+
* The maximum number of entries to return.
149+
* If unspecified, at most 1,000 entries will be returned.
150+
*/
151+
pageSize?: number;
152+
/**
153+
* A page token, received from a previous `ListAndroidDeliveryDataRequest` call.
154+
* Provide this to retrieve the subsequent page.
155+
*/
156+
pageToken?: string;
157+
}
158+
159+
/**
160+
* Response message for ListAndroidDeliveryData.
161+
*/
162+
export interface ListAndroidDeliveryDataResponse {
163+
/**
164+
* The delivery data for the provided app.
165+
* There will be one entry per combination of app, date, and analytics label.
166+
*/
167+
androidDeliveryData: AndroidDeliveryData[];
168+
/**
169+
* A token, which can be sent as `page_token` to retrieve the next page.
170+
* If this field is omitted, there are no subsequent pages.
171+
*/
172+
nextPageToken?: string;
173+
}

0 commit comments

Comments
 (0)