Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,32 @@

let commandScopes = new Set<string>();

export const authProxyOrigin = () =>

Check warning on line 8 in src/api.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
utils.envOverride("FIREBASE_AUTHPROXY_URL", "https://auth.firebase.tools");
// "In this context, the client secret is obviously not treated as a secret"
// https://developers.google.com/identity/protocols/OAuth2InstalledApp
export const clientId = () =>

Check warning on line 12 in src/api.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
utils.envOverride(
"FIREBASE_CLIENT_ID",
"563584335869-fgrhgmd47bqnekij5i8b5pr03ho849e6.apps.googleusercontent.com",
);
export const clientSecret = () =>

Check warning on line 17 in src/api.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
utils.envOverride("FIREBASE_CLIENT_SECRET", "j9iVZfS8kkCEFUPaAeJV0sAi");
export const cloudbillingOrigin = () =>

Check warning on line 19 in src/api.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
utils.envOverride("FIREBASE_CLOUDBILLING_URL", "https://cloudbilling.googleapis.com");
export const cloudloggingOrigin = () =>

Check warning on line 21 in src/api.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
utils.envOverride("FIREBASE_CLOUDLOGGING_URL", "https://logging.googleapis.com");
export const cloudMonitoringOrigin = () =>

Check warning on line 23 in src/api.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
utils.envOverride("CLOUD_MONITORING_URL", "https://monitoring.googleapis.com");
export const containerRegistryDomain = () =>

Check warning on line 25 in src/api.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
utils.envOverride("CONTAINER_REGISTRY_DOMAIN", "gcr.io");

export const developerConnectOrigin = () =>

Check warning on line 28 in src/api.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
utils.envOverride("DEVELOPERCONNECT_URL", "https://developerconnect.googleapis.com");
export const developerConnectP4SADomain = () =>

Check warning on line 30 in src/api.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
utils.envOverride("DEVELOPERCONNECT_P4SA_DOMAIN", "gcp-sa-devconnect.iam.gserviceaccount.com");

export const artifactRegistryDomain = () =>

Check warning on line 33 in src/api.ts

View workflow job for this annotation

GitHub Actions / lint (20)

Missing return type on function
utils.envOverride("ARTIFACT_REGISTRY_DOMAIN", "https://artifactregistry.googleapis.com");
export const appDistributionOrigin = () =>
utils.envOverride(
Expand Down Expand Up @@ -125,6 +125,8 @@
utils.envOverride("FIREBASE_RTDB_METADATA_URL", "https://metadata-dot-firebase-prod.appspot.com");
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 resourceManagerOrigin = () =>
utils.envOverride("FIREBASE_RESOURCEMANAGER_URL", "https://cloudresourcemanager.googleapis.com");
export const rulesOrigin = () =>
Expand Down
2 changes: 2 additions & 0 deletions src/mcp/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { firestoreTools } from "./firestore/index.js";
import { directoryTools } from "./directory/index.js";
import { coreTools } from "./core/index.js";
import { storageTools } from "./storage/index.js";
import { messagingTools } from "./messaging/index.js";

/** availableTools returns the list of MCP tools available given the server flags */
export function availableTools(fixedRoot: boolean, activeFeatures?: ServerFeature[]): ServerTool[] {
Expand All @@ -29,6 +30,7 @@ const tools: Record<ServerFeature, ServerTool[]> = {
auth: addPrefixToToolName("auth_", authTools),
dataconnect: addPrefixToToolName("dataconnect_", dataconnectTools),
storage: addPrefixToToolName("storage_", storageTools),
messaging: addPrefixToToolName("messaging_", messagingTools),
};

function addPrefixToToolName(prefix: string, tools: ServerTool[]): ServerTool[] {
Expand Down
5 changes: 5 additions & 0 deletions src/mcp/tools/messaging/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ServerTool } from "../../tool.js";
import { send_message_to_fcm_token } from "./send_message_to_fcm_token.js";
import { send_message_to_fcm_topic } from "./send_message_to_fcm_topic.js";

export const messagingTools: ServerTool[] = [send_message_to_fcm_token, send_message_to_fcm_topic];
30 changes: 30 additions & 0 deletions src/mcp/tools/messaging/send_message_to_fcm_token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { z } from "zod";
import { tool } from "../../tool.js";
import { mcpError, toContent } from "../../util.js";
import { sendMessageToFcmToken } from "../../../messaging/sendMessage.js";

export const send_message_to_fcm_token = tool(
{
name: "send_message_to_fcm_token",
description: "Sends a message to FCM Token",
inputSchema: z.object({
fcmToken: z.string(),
title: z.string().optional(),
body: z.string().optional(),
}),
annotations: {
title: "Send message to FCM Token",
readOnlyHint: true,
},
_meta: {
requiresAuth: true,
requiresProject: true,
},
},
async ({ fcmToken, title, body }, { projectId }) => {
if (fcmToken === undefined) {
return mcpError(`No fcmToken specified in the send_message_to_fcm_token tool`);
}
return toContent(await sendMessageToFcmToken(projectId!, fcmToken, title, body));
},
);
30 changes: 30 additions & 0 deletions src/mcp/tools/messaging/send_message_to_fcm_topic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { z } from "zod";
import { tool } from "../../tool.js";
import { mcpError, toContent } from "../../util.js";
import { sendMessageToFcmTopic } from "../../../messaging/sendMessage.js";

export const send_message_to_fcm_topic = tool(
{
name: "send_message_to_fcm_topic",
description: "Sends a message to an FCM Topic",
inputSchema: z.object({
topic: z.string(),
title: z.string().optional(),
body: z.string().optional(),
}),
annotations: {
title: "Send message to an FCM Topic",
readOnlyHint: true,
},
_meta: {
requiresAuth: true,
requiresProject: true,
},
},
async ({ topic, title, body }, { projectId }) => {
if (topic === undefined) {
return mcpError(`No topic specified in the send_message_to_fcm_topic tool`);
}
return toContent(await sendMessageToFcmTopic(projectId!, topic, title, body));
},
);
8 changes: 7 additions & 1 deletion src/mcp/types.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
export const SERVER_FEATURES = ["firestore", "storage", "dataconnect", "auth"] as const;
export const SERVER_FEATURES = [
"firestore",
"storage",
"dataconnect",
"auth",
"messaging",
] as const;
export type ServerFeature = (typeof SERVER_FEATURES)[number];
9 changes: 8 additions & 1 deletion src/mcp/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ import { execSync } from "child_process";
import { dump } from "js-yaml";
import { platform } from "os";
import { ServerFeature } from "./types";
import { authManagementOrigin, dataconnectOrigin, firestoreOrigin, storageOrigin } from "../api";
import {
authManagementOrigin,
dataconnectOrigin,
firestoreOrigin,
messagingApiOrigin,
storageOrigin,
} from "../api";
import { check } from "../ensureApiEnabled";

export function toContent(data: any, options?: { format: "json" | "yaml" }): CallToolResult {
Expand Down Expand Up @@ -62,6 +68,7 @@ const SERVER_FEATURE_APIS: Record<ServerFeature, string> = {
storage: storageOrigin(),
dataconnect: dataconnectOrigin(),
auth: authManagementOrigin(),
messaging: messagingApiOrigin(),
};
/**
* Detects whether an MCP feature is active in the current project root. Relies first on
Expand Down
31 changes: 31 additions & 0 deletions src/messaging/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export interface BaseMessage {
notification?: Notification;
}

export interface TokenMessage extends BaseMessage {
token: string;
}

export interface TopicMessage extends BaseMessage {
topic: string;
}

/**
* Payload for the {@link Messaging.send} operation. The payload contains all the fields
* in the BaseMessage type, and exactly one of token, topic or condition.
*/
export type Message = TokenMessage | TopicMessage;

/**
* A notification that can be included in {@link Message}.
*/
export interface Notification {
/**
* The title of the notification.
*/
title?: string;
/**
* The notification body
*/
body?: string;
}
97 changes: 97 additions & 0 deletions src/messaging/sendMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { messagingApiOrigin } from "../api";
import { Client } from "../apiv2";
import { logger } from "../logger";
import { FirebaseError } from "../error";
import { Notification, TokenMessage, TopicMessage } from "./interfaces";

const TIMEOUT = 10000;

const apiClient = new Client({
urlPrefix: messagingApiOrigin(),
apiVersion: "v1",
});

/**
* Function to send a message to an FCM Token.
* @param projectId Project ID to which this token belongs to.
* @param fcmToken The FCM Token to send to.
* @param title The title of the message.
* @param body The body of the message.
* @return {Promise} Returns a promise fulfilled with a unique message ID string
* after the message has been successfully handed off to the FCM service for delivery.
*/
export async function sendMessageToFcmToken(
projectId: string,
fcmToken: string,
title?: string,
body?: string,
): Promise<string> {
try {
const notification: Notification = {
title: title,
body: body,
};
const message: TokenMessage = {
token: fcmToken,
notification: notification,
};
const messageData = {
message: message,
};
const res = await apiClient.request<null, string>({
method: "POST",
path: `/projects/${projectId}/messages:send`,
body: JSON.stringify(messageData),
timeout: TIMEOUT,
});
return res.body;
} catch (err: any) {
logger.debug(err.message);
throw new FirebaseError(
`Failed to send message to ${fcmToken} for the project ${projectId}. `,
{ original: err },
);
}
}

/**
* Function to send a message to an FCM topic. This will initiate a message fanout to the topic members.
* @param projectId Project ID to which this token belongs to.
* @param fcmToken The FCM Token to send to.
* @param title The title of the message.
* @param body The body of the message.
* @return {Promise} Returns a promise fulfilled with a unique message ID string
* after the message has been successfully handed off to the FCM service for delivery.
*/
export async function sendMessageToFcmTopic(
projectId: string,
topic: string,
title?: string,
body?: string,
): Promise<string> {
try {
const notification: Notification = {
title: title,
body: body,
};
const message: TopicMessage = {
topic: topic,
notification: notification,
};
const messageData = {
message: message,
};
const res = await apiClient.request<null, string>({
method: "POST",
path: `/projects/${projectId}/messages:send`,
body: JSON.stringify(messageData),
timeout: TIMEOUT,
});
return res.body;
} catch (err: any) {
logger.debug(err.message);
throw new FirebaseError(`Failed to send message for the project ${projectId}. `, {
original: err,
});
}
}
Loading