Firebase MCP

by gannonh
Verified
/** * Firebase Firestore Client * * This module provides functions for interacting with Firebase Firestore database. * It includes operations for listing collections, querying documents, and performing CRUD operations. * All functions return data in a format compatible with the MCP protocol response structure. * * @module firebase-mcp/firestore */ import { Query, Timestamp } from 'firebase-admin/firestore'; import {db, getProjectId} from './firebaseConfig'; import fs from 'fs'; import path from 'path'; /** * Lists collections in Firestore, either at the root level or under a specific document. * Results are paginated and include links to the Firebase console. * * @param {string} [documentPath] - Optional path to a document to list subcollections * @param {number} [limit=20] - Maximum number of collections to return * @param {string} [pageToken] - Token for pagination (collection ID to start after) * @returns {Promise<Object>} MCP-formatted response with collection data * @throws {Error} If Firebase is not initialized or if there's a Firestore error * * @example * // List root collections * const rootCollections = await list_collections(); * * @example * // List subcollections of a document * const subCollections = await list_collections('users/user123'); */ export async function list_collections(documentPath?: string, limit: number = 20, pageToken?: string) { try { // Check if Firebase is initialized if (!db) { return { content: [{ type: 'text', text: 'Firebase is not initialized. SERVICE_ACCOUNT_KEY_PATH environment variable is required.' }], isError: true }; } let collections; if (documentPath) { // Get subcollections of a specific document const docRef = db.doc(documentPath); collections = await docRef.listCollections(); } else { // Get root collections collections = await db.listCollections(); } // Sort collections by name for consistent ordering collections.sort((a, b) => a.id.localeCompare(b.id)); // Find start index for pagination const startIndex = pageToken ? collections.findIndex(c => c.id === pageToken) + 1 : 0; // Apply limit for pagination const paginatedCollections = collections.slice(startIndex, startIndex + limit); // Get project ID for console URLs const projectId = getProjectId(); const collectionData = paginatedCollections.map((collection) => { const collectionUrl = `https://console.firebase.google.com/project/${projectId}/firestore/data/${documentPath}/${collection.id}`; return { name: collection.id, url: collectionUrl }; }); // Format response for MCP return { content: [{ type: 'text', text: JSON.stringify({ collections: collectionData, nextPageToken: collections.length > startIndex + limit ? paginatedCollections[paginatedCollections.length - 1].id : null, hasMore: collections.length > startIndex + limit }) }] }; } catch (error) { return { content: [{ type: 'text', text: `Error listing collections: ${(error as Error).message}` }], isError: true }; } } /** * Converts Firestore Timestamp objects to ISO string format for JSON serialization. * This is a helper function used internally by other functions. * * @param {any} data - The data object containing potential Timestamp fields * @returns {any} The same data object with Timestamps converted to ISO strings * @private */ function convertTimestampsToISO(data: any) { for (const key in data) { if (data[key] instanceof Timestamp) { data[key] = data[key].toDate().toISOString(); } } return data; } /** * Lists documents in a Firestore collection with optional filtering and pagination. * Results include document data, IDs, and links to the Firebase console. * * @param {string} collection - The collection path to query * @param {Array<Object>} [filters=[]] - Array of filter conditions with field, operator, and value * @param {number} [limit=20] - Maximum number of documents to return * @param {string} [pageToken] - Token for pagination (document ID to start after) * @returns {Promise<Object>} MCP-formatted response with document data * @throws {Error} If Firebase is not initialized or if there's a Firestore error * * @example * // List all documents in a collection * const allDocs = await listDocuments('users'); * * @example * // List documents with filtering * const filteredDocs = await listDocuments('users', [ * { field: 'age', operator: '>=', value: 21 }, * { field: 'status', operator: '==', value: 'active' } * ]); */ export async function listDocuments(collection: string, filters: Array<{ field: string, operator: FirebaseFirestore.WhereFilterOp, value: any }> = [], limit: number = 20, pageToken?: string) { const projectId = getProjectId(); try { // Check if Firebase is initialized if (!db) { return { content: [{ type: 'text', text: 'Firebase is not initialized. SERVICE_ACCOUNT_KEY_PATH environment variable is required.' }], isError: true }; } // Get reference to the collection const collectionRef = db.collection(collection); let filteredQuery: Query = collectionRef; // Apply filters for (const filter of filters) { let filterValue = filter.value; // Convert string dates to Firestore Timestamps if (typeof filterValue === 'string' && !isNaN(Date.parse(filterValue))) { filterValue = Timestamp.fromDate(new Date(filterValue)); } filteredQuery = filteredQuery.where(filter.field, filter.operator, filterValue); } // Apply pagination if a page token is provided if (pageToken) { const startAfterDoc = await collectionRef.doc(pageToken).get(); filteredQuery = filteredQuery.startAfter(startAfterDoc); } // Get total count of documents matching the filter const countSnapshot = await filteredQuery.get(); const totalCount = countSnapshot.size; // Get the documents with limit applied const limitedQuery = filteredQuery.limit(limit); const snapshot = await limitedQuery.get(); // Handle empty results if (snapshot.empty) { return { content: [{ type: 'text', text: 'No matching documents found' }], isError: true }; } // Process document data const documents = snapshot.docs.map((doc: any) => { const data = doc.data(); convertTimestampsToISO(data); const consoleUrl = `https://console.firebase.google.com/project/${projectId}/firestore/data/${collection}/${doc.id}`; return { id: doc.id, url: consoleUrl, document: data }; }); // Format response for MCP return { content: [{ type: 'text', text: JSON.stringify({ totalCount, documents, pageToken: documents.length > 0 ? documents[documents.length - 1].id : null, hasMore: totalCount > limit }) }] }; } catch (error) { return { content: [{ type: 'text', text: `Error listing documents: ${(error as Error).message}` }], isError: true }; } } /** * Adds a new document to a Firestore collection with auto-generated ID. * * @param {string} collection - The collection path to add the document to * @param {any} data - The document data to add * @returns {Promise<Object>} MCP-formatted response with the new document ID and data * @throws {Error} If Firebase is not initialized or if there's a Firestore error * * @example * // Add a new user document * const result = await addDocument('users', { * name: 'John Doe', * email: 'john@example.com', * createdAt: new Date() * }); */ export async function addDocument(collection: string, data: any) { try { // Check if Firebase is initialized if (!db) { return { content: [{ type: 'text', text: 'Firebase is not initialized. SERVICE_ACCOUNT_KEY_PATH environment variable is required.' }], isError: true }; } // Add the document and get its reference const docRef = await db.collection(collection).add(data); const projectId = getProjectId(); // Convert timestamps for JSON serialization convertTimestampsToISO(data); // Generate console URL for the new document const consoleUrl = `https://console.firebase.google.com/project/${projectId}/firestore/data/${collection}/${docRef.id}`; // Format response for MCP return { content: [{ type: 'text', text: JSON.stringify({ id: docRef.id, url: consoleUrl, document: data }) }] }; } catch (error) { return { content: [{ type: 'text', text: `Error adding document: ${(error as Error).message}` }], isError: true }; } } /** * Retrieves a specific document from a Firestore collection by ID. * * @param {string} collection - The collection path containing the document * @param {string} id - The document ID to retrieve * @returns {Promise<Object>} MCP-formatted response with the document data * @throws {Error} If Firebase is not initialized or if there's a Firestore error * * @example * // Get a specific user document * const user = await getDocument('users', 'user123'); */ export async function getDocument(collection: string, id: string) { try { // Check if Firebase is initialized if (!db) { return { content: [{ type: 'text', text: 'Firebase is not initialized. SERVICE_ACCOUNT_KEY_PATH environment variable is required.' }], isError: true }; } // Get the document const doc = await db.collection(collection).doc(id).get(); // Handle document not found if (!doc.exists) { return { content: [{ type: 'text', text: 'Document not found' }], isError: true }; } // Get project ID for console URL const projectId = getProjectId(); const data = doc.data(); // Convert timestamps for JSON serialization convertTimestampsToISO(data); // Generate console URL for the document const consoleUrl = `https://console.firebase.google.com/project/${projectId}/firestore/data/${collection}/${id}`; // Format response for MCP return { content: [{ type: 'text', text: JSON.stringify({ id, url: consoleUrl, document: data }) }] }; } catch (error) { return { content: [{ type: 'text', text: `Error getting document: ${(error as Error).message}` }], isError: true }; } } /** * Updates an existing document in a Firestore collection. * * @param {string} collection - The collection path containing the document * @param {string} id - The document ID to update * @param {any} data - The document data to update (fields will be merged) * @returns {Promise<Object>} MCP-formatted response with the updated document data * @throws {Error} If Firebase is not initialized or if there's a Firestore error * * @example * // Update a user's status * const result = await updateDocument('users', 'user123', { * status: 'inactive', * lastUpdated: new Date() * }); */ export async function updateDocument(collection: string, id: string, data: any) { try { // Check if Firebase is initialized if (!db) { return { content: [{ type: 'text', text: 'Firebase is not initialized. SERVICE_ACCOUNT_KEY_PATH environment variable is required.' }], isError: true }; } // Update the document await db.collection(collection).doc(id).update(data); // Get project ID for console URL const projectId = getProjectId(); // Convert timestamps for JSON serialization convertTimestampsToISO(data); // Generate console URL for the document const consoleUrl = `https://console.firebase.google.com/project/${projectId}/firestore/data/${collection}/${id}`; // Format response for MCP return { content: [{ type: 'text', text: JSON.stringify({ id, url: consoleUrl, document: data }) }] }; } catch (error) { return { content: [{ type: 'text', text: `Error updating document: ${(error as Error).message}` }], isError: true }; } } /** * Deletes a document from a Firestore collection. * * @param {string} collection - The collection path containing the document * @param {string} id - The document ID to delete * @returns {Promise<Object>} MCP-formatted response confirming deletion * @throws {Error} If Firebase is not initialized or if there's a Firestore error * * @example * // Delete a user document * const result = await deleteDocument('users', 'user123'); */ export async function deleteDocument(collection: string, id: string) { try { // Check if Firebase is initialized if (!db) { return { content: [{ type: 'text', text: 'Firebase is not initialized. SERVICE_ACCOUNT_KEY_PATH environment variable is required.' }], isError: true }; } // Delete the document await db.collection(collection).doc(id).delete(); // Format response for MCP return { content: [{ type: 'text', text: 'Document deleted successfully' }] }; } catch (error) { return { content: [{ type: 'text', text: `Error deleting document: ${(error as Error).message}` }], isError: true }; } }