Skip to main content
Glama

Firebase MCP

firestoreClient.ts25.2 kB
/** * 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 { Timestamp } from 'firebase-admin/firestore'; import { getProjectId } from './firebaseConfig.js'; import * as admin from 'firebase-admin'; import { logger } from '../../utils/logger.js'; interface FirestoreResponse { content: Array<{ type: string; text: string }>; isError?: boolean; } /** * Executes a function with stdout filtering to prevent Firebase SDK debug output * from interfering with JSON-RPC communication. * * @param fn The function to execute with filtered stdout * @returns The result of the function */ async function withFilteredStdout<T>(fn: () => Promise<T>): Promise<T> { // Save the original stdout.write const originalStdoutWrite = process.stdout.write.bind(process.stdout); // Debug counters let filteredMessages = 0; // Create a filtered version // eslint-disable-next-line @typescript-eslint/no-explicit-any process.stdout.write = function (this: any, chunk: any, ...args: any[]): boolean { // Convert chunk to string if it's a buffer const str = Buffer.isBuffer(chunk) ? chunk.toString() : String(chunk); // Check if this is a Firebase SDK debug message if ( str.includes('parent:') || str.includes('pageSize:') || str.includes('CallSettings') || str.includes('retry:') ) { // Skip writing this to stdout filteredMessages++; // Log filtered messages for debugging (not to stdout) logger.debug(`Filtered Firebase SDK debug message: ${str.substring(0, 50)}...`); // Call the callback if provided const callback = args.length >= 2 ? args[1] : args[0]; if (typeof callback === 'function') { callback(); } return true; } // Otherwise, call the original method return originalStdoutWrite(chunk, ...args); }; try { // Execute the function return await fn(); } finally { // Restore the original stdout.write process.stdout.write = originalStdoutWrite; // Log how many messages were filtered if (filteredMessages > 0) { logger.debug(`Filtered ${filteredMessages} Firebase SDK debug messages`); } } } /** * 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 (currently unused) * @param {string} [_pageToken] - Token for pagination (collection ID to start after) (currently unused) * @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, // eslint-disable-next-line @typescript-eslint/no-explicit-any adminInstance?: any ): Promise<FirestoreResponse> { try { // Use the passed admin instance if available, otherwise use the imported one const adminToUse = adminInstance || admin; // Check if Firebase admin is properly initialized if (!adminToUse || typeof adminToUse.firestore !== 'function') { return { content: [{ type: 'text', text: JSON.stringify({ error: 'Firebase not initialized' }) }], isError: true, }; } // Get the Firestore instance const firestore = adminToUse.firestore(); if (!firestore) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Firestore instance not available' }) }, ], isError: true, }; } // Get the service account path for project ID const serviceAccountPath = process.env.SERVICE_ACCOUNT_KEY_PATH; if (!serviceAccountPath) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Service account path not set' }) }, ], isError: true, }; } const projectId = getProjectId(serviceAccountPath); if (!projectId) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Could not determine project ID' }) }, ], isError: true, }; } // Create a safe array to hold collection information const safeCollections: Array<{ id: string; path: string; url: string }> = []; try { // Get collections using the Firestore API with stdout filtering logger.debug('Calling listCollections() with stdout filtering'); // Use our utility function to filter stdout during the listCollections() call const collectionsRef = await withFilteredStdout(async () => { return documentPath ? await firestore.doc(documentPath).listCollections() : await firestore.listCollections(); }); logger.debug(`Successfully retrieved collections with stdout filtering`); // Important: Convert the collection references to a simple array of objects // This avoids any issues with circular references or non-serializable properties for (const collection of collectionsRef) { // Extract only the string properties we need const id = String(collection.id || ''); const path = String(collection.path || ''); const collectionUrl = `https://console.firebase.google.com/project/${projectId}/firestore/data/${documentPath || ''}${documentPath ? '/' : ''}${id}`; // Add to our safe array safeCollections.push({ id: id, path: path, url: collectionUrl, }); } } catch (collectionError) { // Log the error but continue with an empty array return { content: [ { type: 'text', text: JSON.stringify({ error: 'Error listing collections', message: collectionError instanceof Error ? collectionError.message : 'Unknown error', collections: [], }), }, ], isError: true, }; } // Create a result object with our safe collections const result = { collections: safeCollections, path: documentPath || 'root', projectId: projectId, }; // Return a clean JSON response return { content: [{ type: 'text', text: JSON.stringify(result) }], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; // Always use 'text' type for error responses too return { content: [{ type: 'text', text: JSON.stringify({ error: errorMessage }) }], isError: true, }; } } /** * Converts Firestore Timestamp objects to ISO string format for JSON serialization. * This is a helper function used internally by other functions. * * @param data - The data object containing potential Timestamp fields * @returns The same data object with Timestamps converted to ISO strings * @private */ function convertTimestampsToISO(data: Record<string, unknown>): Record<string, unknown> { 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: unknown }>, limit: number = 20, pageToken?: string ): Promise<FirestoreResponse> { try { // Check if Firebase admin is properly initialized if (!admin || typeof admin.firestore !== 'function') { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Firebase is not properly initialized' }) }, ], isError: true, }; } // Get the Firestore instance const firestore = admin.firestore(); if (!firestore) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Firestore instance not available' }) }, ], isError: true, }; } let query: FirebaseFirestore.Query = firestore.collection(collection); if (filters) { filters.forEach(filter => { query = query.where(filter.field, filter.operator, filter.value); }); } if (limit) { query = query.limit(limit); } if (pageToken) { const lastDoc = await firestore.doc(pageToken).get(); if (lastDoc.exists) { query = query.startAfter(lastDoc); } } const snapshot = await query.get(); const serviceAccountPath = process.env.SERVICE_ACCOUNT_KEY_PATH; if (!serviceAccountPath) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Service account path not set' }) }, ], isError: true, }; } const projectId = getProjectId(serviceAccountPath); if (!projectId) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Could not determine project ID' }) }, ], isError: true, }; } const documents = snapshot.docs.map(doc => { const consoleUrl = `https://console.firebase.google.com/project/${projectId}/firestore/data/${collection}/${doc.id}`; return { id: doc.id, data: doc.data(), url: consoleUrl, }; }); const lastVisible = snapshot.docs[snapshot.docs.length - 1]; const nextPageToken = lastVisible ? lastVisible.ref.path : undefined; return { content: [{ type: 'text', text: JSON.stringify({ documents, nextPageToken }) }], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { content: [{ type: 'text', text: JSON.stringify({ error: errorMessage }) }], 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: object): Promise<FirestoreResponse> { try { // Check if Firebase admin is properly initialized if (!admin || typeof admin.firestore !== 'function') { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Firebase is not properly initialized' }) }, ], isError: true, }; } // Get the Firestore instance const firestore = admin.firestore(); if (!firestore) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Firestore instance not available' }) }, ], isError: true, }; } const docRef = await firestore.collection(collection).add(data); const serviceAccountPath = process.env.SERVICE_ACCOUNT_KEY_PATH; if (!serviceAccountPath) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Service account path not set' }) }, ], isError: true, }; } const projectId = getProjectId(serviceAccountPath); if (!projectId) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Could not determine project ID' }) }, ], isError: true, }; } const consoleUrl = `https://console.firebase.google.com/project/${projectId}/firestore/data/${collection}/${docRef.id}`; return { content: [{ type: 'text', text: JSON.stringify({ id: docRef.id, url: consoleUrl }) }], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { content: [{ type: 'text', text: JSON.stringify({ error: errorMessage }) }], 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): Promise<FirestoreResponse> { try { // Check if Firebase admin is properly initialized if (!admin || typeof admin.firestore !== 'function') { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Firebase is not properly initialized' }) }, ], isError: true, }; } // Get the Firestore instance const firestore = admin.firestore(); if (!firestore) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Firestore instance not available' }) }, ], isError: true, }; } const doc = await firestore.collection(collection).doc(id).get(); const serviceAccountPath = process.env.SERVICE_ACCOUNT_KEY_PATH; if (!serviceAccountPath) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Service account path not set' }) }, ], isError: true, }; } const projectId = getProjectId(serviceAccountPath); if (!projectId) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Could not determine project ID' }) }, ], isError: true, }; } if (!doc.exists) { return { content: [{ type: 'text', text: JSON.stringify({ error: `Document not found: ${id}` }) }], isError: true, }; } const consoleUrl = `https://console.firebase.google.com/project/${projectId}/firestore/data/${collection}/${id}`; return { content: [ { type: 'text', text: JSON.stringify({ id: doc.id, data: doc.data(), url: consoleUrl }) }, ], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { content: [{ type: 'text', text: JSON.stringify({ error: errorMessage }) }], 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: object ): Promise<FirestoreResponse> { try { // Check if Firebase admin is properly initialized if (!admin || typeof admin.firestore !== 'function') { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Firebase is not properly initialized' }) }, ], isError: true, }; } // Get the Firestore instance const firestore = admin.firestore(); if (!firestore) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Firestore instance not available' }) }, ], isError: true, }; } await firestore.collection(collection).doc(id).update(data); const serviceAccountPath = process.env.SERVICE_ACCOUNT_KEY_PATH; if (!serviceAccountPath) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Service account path not set' }) }, ], isError: true, }; } const projectId = getProjectId(serviceAccountPath); if (!projectId) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Could not determine project ID' }) }, ], isError: true, }; } const consoleUrl = `https://console.firebase.google.com/project/${projectId}/firestore/data/${collection}/${id}`; return { content: [{ type: 'text', text: JSON.stringify({ success: true, url: consoleUrl }) }], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { content: [{ type: 'text', text: JSON.stringify({ error: errorMessage }) }], 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): Promise<FirestoreResponse> { try { // Check if Firebase admin is properly initialized if (!admin || typeof admin.firestore !== 'function') { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Firebase is not properly initialized' }) }, ], isError: true, }; } // Get the Firestore instance const firestore = admin.firestore(); if (!firestore) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Firestore instance not available' }) }, ], isError: true, }; } const docRef = firestore.collection(collection).doc(id); const doc = await docRef.get(); if (!doc.exists) { return { content: [{ type: 'text', text: JSON.stringify({ error: 'no entity to delete' }) }], isError: true, }; } await docRef.delete(); return { content: [{ type: 'text', text: JSON.stringify({ success: true }) }], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { content: [{ type: 'text', text: JSON.stringify({ error: errorMessage }) }], isError: true, }; } } /** * Queries across all subcollections with the same name regardless of their parent document. * This is useful for searching data across multiple parent documents. * * @param {string} collectionId - The collection ID to query (without parent path) * @param {Array<Object>} [filters=[]] - Array of filter conditions with field, operator, and value * @param {Array<Object>} [orderBy=[]] - Array of fields to order results by * @param {number} [limit=20] - Maximum number of documents to return * @param {string} [pageToken] - Token for pagination (document path 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 * // Query across all 'comments' subcollections * const allComments = await queryCollectionGroup('comments'); * * @example * // Query with filtering * const filteredComments = await queryCollectionGroup('comments', [ * { field: 'rating', operator: '>', value: 3 } * ]); */ export async function queryCollectionGroup( collectionId: string, filters?: Array<{ field: string; operator: FirebaseFirestore.WhereFilterOp; value: unknown }>, orderBy?: Array<{ field: string; direction?: 'asc' | 'desc' }>, limit: number = 20, pageToken?: string ): Promise<FirestoreResponse> { try { // Check if Firebase admin is properly initialized if (!admin || typeof admin.firestore !== 'function') { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Firebase is not properly initialized' }) }, ], isError: true, }; } // Get the Firestore instance const firestore = admin.firestore(); if (!firestore) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Firestore instance not available' }) }, ], isError: true, }; } // Use any to bypass TypeScript type check for collectionGroup // The Firebase types are sometimes inconsistent between versions let query: FirebaseFirestore.Query = firestore.collectionGroup(collectionId); // Apply filters if provided if (filters && filters.length > 0) { filters.forEach(filter => { query = query.where(filter.field, filter.operator, filter.value); }); } // Apply ordering if provided if (orderBy && orderBy.length > 0) { orderBy.forEach(order => { query = query.orderBy(order.field, order.direction || 'asc'); }); } // Apply pagination if pageToken is provided if (pageToken) { try { const lastDoc = await firestore.doc(pageToken).get(); if (lastDoc.exists) { query = query.startAfter(lastDoc); } } catch (error) { return { content: [ { type: 'text', text: JSON.stringify({ error: `Invalid pagination token: ${error instanceof Error ? error.message : 'Unknown error'}`, }), }, ], isError: true, }; } } // Apply limit query = query.limit(limit); const snapshot = await query.get(); const serviceAccountPath = process.env.SERVICE_ACCOUNT_KEY_PATH; if (!serviceAccountPath) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Service account path not set' }) }, ], isError: true, }; } const projectId = getProjectId(serviceAccountPath); if (!projectId) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Could not determine project ID' }) }, ], isError: true, }; } const documents = snapshot.docs.map((doc: FirebaseFirestore.QueryDocumentSnapshot) => { // For collection groups, we need to use the full path for the URL const fullPath = doc.ref.path; const consoleUrl = `https://console.firebase.google.com/project/${projectId}/firestore/data/${fullPath}`; // Handle Timestamp and other Firestore types const data = convertTimestampsToISO(doc.data()); return { id: doc.id, path: fullPath, data, url: consoleUrl, }; }); // Get the last document for pagination const lastVisible = snapshot.docs[snapshot.docs.length - 1]; const nextPageToken = lastVisible ? lastVisible.ref.path : undefined; // Ensure we're creating valid JSON by serializing and handling special characters const responseObj = { documents, nextPageToken }; const jsonText = JSON.stringify(responseObj); return { content: [{ type: 'text', text: jsonText }], }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; return { content: [{ type: 'text', text: JSON.stringify({ error: errorMessage }) }], isError: true, }; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/gannonh/firebase-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server