Skip to main content
Glama

Firebase MCP

index.ts57.1 kB
#!/usr/bin/env node /** * Firebase MCP Server * * This server implements the Model Context Protocol (MCP) for Firebase services. * It provides tools for interacting with Firebase Authentication, Firestore, and Storage * through a standardized interface that can be used by AI assistants and other MCP clients. * * @module firebase-mcp */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js'; import admin from 'firebase-admin'; import { logger } from './utils/logger.js'; import { Timestamp } from 'firebase-admin/firestore'; import config from './config.js'; import { initializeTransport } from './transports/index.js'; import * as fs from 'fs'; // Initialize Firebase async function initializeFirebase(): Promise<admin.app.App | null> { try { const serviceAccountPath = config.serviceAccountKeyPath; if (!serviceAccountPath) { logger.error('SERVICE_ACCOUNT_KEY_PATH not set'); return null; } try { const existingApp = admin.app(); if (existingApp) { logger.debug('Using existing Firebase app'); return existingApp; } } catch { // No existing app, continue with initialization logger.debug('No existing Firebase app, initializing new one'); } // Read the service account key file try { const serviceAccountContent = fs.readFileSync(serviceAccountPath, 'utf8'); logger.debug(`Service account file read successfully: ${serviceAccountPath}`); const serviceAccount = JSON.parse(serviceAccountContent); logger.debug( `Service account parsed successfully: ${Object.keys(serviceAccount).join(', ')}` ); const storageBucket = config.storageBucket || undefined; logger.debug(`Initializing Firebase with storage bucket: ${storageBucket}`); // Initialize Firebase with the service account return admin.initializeApp({ credential: admin.credential.cert(serviceAccount), storageBucket: storageBucket, }); } catch (error) { logger.error( `Error initializing Firebase: ${error instanceof Error ? error.message : 'Unknown error'}` ); return null; } } catch (error) { logger.error('Failed to initialize Firebase', error); return null; } } // Initialize Firebase (will be set asynchronously) let app: admin.app.App | null = null; // Initialize the app asynchronously (async () => { app = await initializeFirebase(); })(); // This interface was previously used but is now handled by the SDK // Keeping it commented for reference // interface FirebaseToolResponse { // content: Array<{ type: string; text: string }>; // isError?: boolean; // } /** * Main server class that implements the MCP protocol for Firebase services. * Handles tool registration, request routing, and server lifecycle. */ class FirebaseMcpServer { /** The MCP server instance */ private server: Server; /** * Initializes the Firebase MCP server with configuration and event handlers. */ constructor() { this.server = new Server( { name: config.name, version: config.version, }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); // Set up error handling and graceful shutdown this.server.onerror = () => {}; process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } /** * Registers all available Firebase tools with the MCP server. * This includes tools for Firestore, Authentication, and Storage operations. * @private */ private setupToolHandlers(): void { // Register available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'firestore_add_document', description: 'Add a document to a Firestore collection', inputSchema: { type: 'object', properties: { collection: { type: 'string', description: 'Collection name', }, data: { type: 'object', description: 'Document data', }, }, required: ['collection', 'data'], }, }, { name: 'firestore_list_documents', description: 'List documents from a Firestore collection with filtering and ordering', inputSchema: { type: 'object', properties: { collection: { type: 'string', description: 'Collection name', }, filters: { type: 'array', description: 'Array of filter conditions', items: { type: 'object', properties: { field: { type: 'string', description: 'Field name to filter', }, operator: { type: 'string', description: 'Comparison operator (==, >, <, >=, <=, array-contains, in, array-contains-any)', }, value: { type: 'string', description: 'Value to compare against (use ISO format for dates)', }, }, required: ['field', 'operator', 'value'], }, }, limit: { type: 'number', description: 'Number of documents to return', default: 20, }, pageToken: { type: 'string', description: 'Token for pagination to get the next page of results', }, orderBy: { type: 'array', description: 'Array of fields to order by', items: { type: 'object', properties: { field: { type: 'string', description: 'Field name to order by', }, direction: { type: 'string', description: 'Sort direction (asc or desc)', enum: ['asc', 'desc'], default: 'asc', }, }, required: ['field'], }, }, }, required: ['collection'], }, }, { name: 'firestore_get_document', description: 'Get a document from a Firestore collection', inputSchema: { type: 'object', properties: { collection: { type: 'string', description: 'Collection name', }, id: { type: 'string', description: 'Document ID', }, }, required: ['collection', 'id'], }, }, { name: 'firestore_update_document', description: 'Update a document in a Firestore collection', inputSchema: { type: 'object', properties: { collection: { type: 'string', description: 'Collection name', }, id: { type: 'string', description: 'Document ID', }, data: { type: 'object', description: 'Updated document data', }, }, required: ['collection', 'id', 'data'], }, }, { name: 'firestore_delete_document', description: 'Delete a document from a Firestore collection', inputSchema: { type: 'object', properties: { collection: { type: 'string', description: 'Collection name', }, id: { type: 'string', description: 'Document ID', }, }, required: ['collection', 'id'], }, }, { name: 'auth_get_user', description: 'Get a user by ID or email from Firebase Authentication', inputSchema: { type: 'object', properties: { identifier: { type: 'string', description: 'User ID or email address', }, }, required: ['identifier'], }, }, { name: 'storage_list_files', description: 'List files in a given path in Firebase Storage', inputSchema: { type: 'object', properties: { directoryPath: { type: 'string', description: 'The optional path to list files from. If not provided, the root is used.', }, }, required: [], }, }, { name: 'storage_get_file_info', description: 'Get file information including metadata and download URL', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'The path of the file to get information for', }, }, required: ['filePath'], }, }, { name: 'storage_upload', description: 'Upload a file to Firebase Storage. Supports local file paths (preferred for binary files), base64 data, or plain text.', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'The destination path in Firebase Storage (e.g., "images/logo.png"). If necessary, rename files for optimal URL compatibility (e.g., "my-document.pdf" rather than "My Document.pdf").', }, content: { type: 'string', description: 'Can be: 1) A local file path (e.g., "/path/to/file.pdf") - RECOMMENDED for all file types, especially binary files like PDFs and images, 2) A data URL (e.g., "data:image/png;base64,...") - may have issues with large files, or 3) Plain text content. Note: Document references are not directly accessible - always use the actual file path instead.', }, contentType: { type: 'string', description: 'Optional MIME type. If not provided, it will be automatically detected', }, metadata: { type: 'object', description: 'Optional additional metadata', }, }, required: ['filePath', 'content'], }, responseFormatting: { template: '## File Successfully Uploaded! 📁\n\nYour file has been uploaded to Firebase Storage:\n\n**File Details:**\n- **Name:** {{name}}\n- **Size:** {{size}} bytes\n- **Type:** {{contentType}}\n- **Last Updated:** {{updated}}\n- **Bucket:** {{bucket}}\n\n**[Click here to download your file]({{downloadUrl}})**\n\nThis is a permanent URL that will not expire.', fields: ['name', 'size', 'contentType', 'updated', 'bucket', 'downloadUrl'], }, }, { name: 'storage_upload_from_url', description: 'Upload a file to Firebase Storage from an external URL. Perfect for images, documents, or any file accessible via URL.', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'The destination path in Firebase Storage (e.g., "images/photo.jpg"). If necessary, rename files for optimal URL compatibility (e.g., "my-document.pdf" rather than "My Document.pdf").', }, url: { type: 'string', description: 'The source URL to download from (e.g., "https://example.com/image.jpg"). For GitHub files, use the raw URL (add ?raw=true)', }, contentType: { type: 'string', description: 'Optional MIME type. If not provided, it will be automatically detected from the URL or response headers', }, metadata: { type: 'object', description: 'Optional additional metadata', }, }, required: ['filePath', 'url'], }, responseFormatting: { template: '## File Successfully Uploaded from URL! 📁\n\nYour file has been uploaded to Firebase Storage:\n\n**File Details:**\n- **Name:** {{name}}\n- **Size:** {{size}} bytes\n- **Type:** {{contentType}}\n- **Last Updated:** {{updated}}\n- **Source URL:** {{sourceUrl}}\n- **Bucket:** {{bucket}}\n\n**[Click here to download your file]({{downloadUrl}})**\n\nThis is a permanent URL that will not expire.', fields: [ 'name', 'size', 'contentType', 'updated', 'sourceUrl', 'bucket', 'downloadUrl', ], }, }, { name: 'firestore_list_collections', description: 'List root collections in Firestore', inputSchema: { type: 'object', properties: {}, required: [], }, }, { name: 'firestore_query_collection_group', description: 'Query documents across all subcollections with the same name (collection group query)', inputSchema: { type: 'object', properties: { collectionId: { type: 'string', description: 'The collection ID to query across all documents (without parent path)', }, filters: { type: 'array', description: 'Optional filters to apply to the query', items: { type: 'object', properties: { field: { type: 'string', description: 'Field name to filter', }, operator: { type: 'string', description: 'Comparison operator (==, !=, <, <=, >, >=, array-contains, array-contains-any, in, not-in)', }, value: { type: 'string', description: 'Value to compare against', }, }, required: ['field', 'operator', 'value'], }, }, orderBy: { type: 'array', description: 'Optional fields to order results by', items: { type: 'object', properties: { field: { type: 'string', description: 'Field name to order by', }, direction: { type: 'string', enum: ['asc', 'desc'], default: 'asc', description: 'Sort direction (asc or desc)', }, }, required: ['field'], }, }, limit: { type: 'number', description: 'Maximum number of documents to return (default: 20, max: 100)', }, pageToken: { type: 'string', description: 'Token for pagination (document path to start after)', }, }, required: ['collectionId'], }, }, ], })); // Handle tool execution this.server.setRequestHandler(CallToolRequestSchema, async request => { const { name, arguments: args = {} } = request.params; try { if (!app) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Firebase initialization failed', }), }, ], }; } switch (name) { case 'firestore_add_document': { const collection = args.collection as string; const data = args.data as Record<string, unknown>; // Process server timestamps and convert ISO date strings to Timestamps const processedData = Object.entries(data).reduce( (acc, [key, value]) => { // Check if this is a server timestamp request if (value && typeof value === 'object' && '__serverTimestamp' in value) { acc[key] = admin.firestore.FieldValue.serverTimestamp(); } // Check if this is an ISO date string that should be converted to a Timestamp else if ( typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value) ) { try { // Convert ISO string to Timestamp for Firestore acc[key] = admin.firestore.Timestamp.fromDate(new Date(value)); logger.debug(`Converted string date to Timestamp: ${value}`); } catch { // If conversion fails, use the original value logger.warn(`Failed to convert date string to Timestamp: ${value}`); acc[key] = value; } } else { acc[key] = value; } return acc; }, {} as Record<string, unknown> ); const docRef = await admin.firestore().collection(collection).add(processedData); // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify({ id: docRef.id, path: docRef.path, }); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug('firestore_add_document response:', JSON.stringify(response)); return response; } case 'firestore_list_documents': { const collection = args.collection as string; const limit = Math.min(Math.max(1, (args.limit as number) || 20), 100); // Default 20, max 100 let query: admin.firestore.Query = admin.firestore().collection(collection); // Apply filters if provided const filters = args.filters as | Array<{ field: string; operator: admin.firestore.WhereFilterOp; value: unknown; }> | undefined; if (filters && filters.length > 0) { filters.forEach(filter => { let filterValue = filter.value; // Check if this might be a timestamp value in ISO format if ( typeof filterValue === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(filterValue) ) { try { // Convert ISO string to Timestamp for Firestore queries filterValue = admin.firestore.Timestamp.fromDate(new Date(filterValue)); } catch { // If conversion fails, use the original value logger.warn(`Failed to convert timestamp string to Timestamp: ${filterValue}`); } } query = query.where(filter.field, filter.operator, filterValue); }); } // Apply ordering if provided const orderBy = args.orderBy as | Array<{ field: string; direction?: 'asc' | 'desc'; }> | undefined; if (orderBy && orderBy.length > 0) { orderBy.forEach(order => { query = query.orderBy(order.field, order.direction || 'asc'); }); } // Apply pagination if pageToken is provided const pageToken = args.pageToken as string | undefined; if (pageToken) { const lastDoc = await admin.firestore().doc(pageToken).get(); if (lastDoc.exists) { query = query.startAfter(lastDoc); } } // Apply limit query = query.limit(limit); const snapshot = await query.get(); const documents = snapshot.docs.map(doc => { const rawData = doc.data(); // Sanitize data to ensure it's JSON-serializable const data = Object.entries(rawData).reduce( (acc, [key, value]) => { // Handle basic types directly if ( typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || value === null ) { acc[key] = value; } // Convert Date objects to ISO strings else if (value instanceof Date) { acc[key] = value.toISOString(); } // Handle Firestore Timestamp objects properly else if (value instanceof Timestamp) { acc[key] = value.toDate().toISOString(); } // Convert arrays to strings else if (Array.isArray(value)) { acc[key] = `[${value.join(', ')}]`; } // Convert other objects to string representation else if (typeof value === 'object') { acc[key] = '[Object]'; } // Convert other types to strings else { acc[key] = String(value); } return acc; }, {} as Record<string, unknown> ); return { id: doc.id, path: doc.ref.path, data, }; }); // Get the last document for pagination const lastVisible = snapshot.docs[snapshot.docs.length - 1]; const nextPageToken = lastVisible ? lastVisible.ref.path : null; // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify({ documents, nextPageToken, }); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug('firestore_list_documents response:', JSON.stringify(response)); return response; } case 'firestore_get_document': { const collection = args.collection as string; const id = args.id as string; const docRef = admin.firestore().collection(collection).doc(id); const doc = await docRef.get(); if (!doc.exists) { // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify({ error: 'Document not found', }); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug('firestore_get_document error response:', JSON.stringify(response)); return response; } const rawData = doc.data(); // Sanitize data to ensure it's JSON-serializable const data = Object.entries(rawData || {}).reduce( (acc, [key, value]) => { if ( typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || value === null ) { acc[key] = value; } else if (value instanceof Date) { acc[key] = value.toISOString(); } else if (value instanceof Timestamp) { acc[key] = value.toDate().toISOString(); } else if (Array.isArray(value)) { acc[key] = `[${value.join(', ')}]`; } else if (typeof value === 'object') { acc[key] = '[Object]'; } else { acc[key] = String(value); } return acc; }, {} as Record<string, unknown> ); // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify({ id: doc.id, path: doc.ref.path, data, }); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug('firestore_get_document response:', JSON.stringify(response)); return response; } case 'firestore_update_document': { const collection = args.collection as string; const id = args.id as string; const data = args.data as Record<string, unknown>; // Process server timestamps and convert ISO date strings to Timestamps const processedData = Object.entries(data).reduce( (acc, [key, value]) => { // Check if this is a server timestamp request if (value && typeof value === 'object' && '__serverTimestamp' in value) { acc[key] = admin.firestore.FieldValue.serverTimestamp(); } // Check if this is an ISO date string that should be converted to a Timestamp else if ( typeof value === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value) ) { try { // Convert ISO string to Timestamp for Firestore acc[key] = admin.firestore.Timestamp.fromDate(new Date(value)); logger.debug(`Converted string date to Timestamp: ${value}`); } catch { // If conversion fails, use the original value logger.warn(`Failed to convert date string to Timestamp: ${value}`); acc[key] = value; } } else { acc[key] = value; } return acc; }, {} as Record<string, unknown> ); const docRef = admin.firestore().collection(collection).doc(id); await docRef.update(processedData); // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify({ id, path: docRef.path, updated: true, }); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug('firestore_update_document response:', JSON.stringify(response)); return response; } case 'firestore_delete_document': { const collection = args.collection as string; const id = args.id as string; const docRef = admin.firestore().collection(collection).doc(id); await docRef.delete(); // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify({ id, path: docRef.path, deleted: true, }); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug('firestore_delete_document response:', JSON.stringify(response)); return response; } case 'auth_get_user': { const identifier = args.identifier as string; try { let user; // Try to get user by email first if (identifier.includes('@')) { const userByEmail = await admin.auth().getUserByEmail(identifier); user = userByEmail; } else { // If not an email, try by UID const userById = await admin.auth().getUser(identifier); user = userById; } // Sanitize user data to ensure it's JSON-serializable const sanitizedUser = { uid: user.uid, email: user.email, emailVerified: user.emailVerified, displayName: user.displayName, photoURL: user.photoURL, disabled: user.disabled, metadata: { creationTime: user.metadata.creationTime, lastSignInTime: user.metadata.lastSignInTime, }, }; // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify({ user: sanitizedUser }); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug('auth_get_user response:', JSON.stringify(response)); return response; } catch (error) { // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify({ error: 'User not found', details: error instanceof Error ? error.message : 'Unknown error', }); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug('auth_get_user error response:', JSON.stringify(response)); return response; } } case 'storage_list_files': { const directoryPath = (args.directoryPath as string) || ''; try { logger.debug(`Listing files in directory: ${directoryPath}`); const bucket = admin.storage().bucket(); logger.debug(`Got bucket reference: ${bucket.name}`); const [files] = await bucket.getFiles({ prefix: directoryPath, delimiter: '/', }); logger.debug(`Found ${files.length} files`); const fileList = files.map(file => ({ name: file.name, size: file.metadata.size ? file.metadata.size : '0', contentType: file.metadata.contentType || null, updated: file.metadata.updated || null, md5Hash: file.metadata.md5Hash || null, })); // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify({ files: fileList }); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug('storage_list_files response:', JSON.stringify(response)); return response; } catch (error) { logger.error('Failed to list files', error); // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify({ error: 'Failed to list files', details: error instanceof Error ? error.message : 'Unknown error', }); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug('storage_list_files error response:', JSON.stringify(response)); return response; } } case 'storage_get_file_info': { const filePath = args.filePath as string; try { logger.debug(`Getting info for file: ${filePath}`); const bucket = admin.storage().bucket(); logger.debug(`Got bucket reference: ${bucket.name}`); const file = bucket.file(filePath); const [exists] = await file.exists(); if (!exists) { logger.warn(`File not found: ${filePath}`); // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify({ error: 'File not found', }); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug('storage_get_file_info error response:', JSON.stringify(response)); return response; } logger.debug('File exists, getting metadata and signed URL'); const [metadata] = await file.getMetadata(); const [url] = await file.getSignedUrl({ action: 'read', expires: Date.now() + 15 * 60 * 1000, // URL expires in 15 minutes }); const fileInfo = { name: file.name, bucket: file.bucket.name, size: metadata.size || '0', contentType: metadata.contentType || null, updated: metadata.updated || null, md5Hash: metadata.md5Hash || null, downloadUrl: url, }; // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify(fileInfo); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug('storage_get_file_info response:', JSON.stringify(response)); return response; } catch (error) { logger.error('Failed to get file info', error); // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify({ error: 'Failed to get file info', details: error instanceof Error ? error.message : 'Unknown error', }); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug('storage_get_file_info error response:', JSON.stringify(response)); return response; } } case 'storage_upload': { const { filePath, content, contentType, metadata } = args; try { logger.debug(`Uploading file to: ${filePath}`); const storageClient = await import('./lib/firebase/storageClient.js'); const uploadFile = storageClient.uploadFile; const result = await uploadFile( filePath as string, content as string, contentType as string | undefined, metadata as Record<string, unknown> | undefined ); // Check if there's an error if (result.isError) { // Log the response for debugging const response = { content: [ { type: 'text', text: result.content[0].text, }, ], error: true, }; logger.debug('storage_upload error response:', JSON.stringify(response)); return response; } // Extract the file info from the JSON response try { const fileInfo = JSON.parse(result.content[0].text); // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify(fileInfo); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug('storage_upload success response:', JSON.stringify(response)); return response; } catch { // If parsing fails, return the original text // Log the response for debugging const response = { content: [ { type: 'text', text: result.content[0].text, }, ], }; logger.debug('storage_upload fallback response:', JSON.stringify(response)); return response; } } catch (error) { logger.error('Failed to upload file', error); // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify({ error: 'Failed to upload file', details: error instanceof Error ? error.message : 'Unknown error', }); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug('storage_upload catch error response:', JSON.stringify(response)); return response; } } case 'storage_upload_from_url': { const { filePath, url, contentType, metadata } = args; try { logger.debug(`Uploading file from URL: ${url} to: ${filePath}`); const storageClient = await import('./lib/firebase/storageClient.js'); const uploadFileFromUrl = storageClient.uploadFileFromUrl; const result = await uploadFileFromUrl( filePath as string, url as string, contentType as string | undefined, metadata as Record<string, unknown> | undefined ); // Check if there's an error if (result.isError) { // Log the response for debugging const response = { content: [ { type: 'text', text: result.content[0].text, }, ], error: true, }; logger.debug('storage_upload_from_url error response:', JSON.stringify(response)); return response; } // Extract the file info from the JSON response try { const fileInfo = JSON.parse(result.content[0].text); // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify(fileInfo); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug('storage_upload_from_url success response:', JSON.stringify(response)); return response; } catch { // If parsing fails, return the original text // Log the response for debugging const response = { content: [ { type: 'text', text: result.content[0].text, }, ], }; logger.debug( 'storage_upload_from_url fallback response:', JSON.stringify(response) ); return response; } } catch (error) { logger.error('Failed to upload file from URL', error); // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify({ error: 'Failed to upload file from URL', details: error instanceof Error ? error.message : 'Unknown error', }); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug( 'storage_upload_from_url catch error response:', JSON.stringify(response) ); return response; } } case 'firestore_list_collections': { try { logger.debug('Listing Firestore collections'); // Get document path parameter if provided const documentPath = args.documentPath as string | undefined; // Make sure admin is properly initialized if (!app) { logger.error('Firebase app is not initialized'); return { content: [ { type: 'text', text: JSON.stringify({ error: 'Firebase initialization failed', }), }, ], }; } // Import the standardized implementation from firestoreClient const firestoreClient = await import('./lib/firebase/firestoreClient.js'); // Call the standardized implementation with the document path and admin instance const result = await firestoreClient.list_collections( documentPath, undefined, undefined, admin ); // Log the response for debugging logger.debug(`Found collections in response`); // Return the response directly without additional parsing // This avoids any potential issues with double parsing return { content: [ { type: 'text', text: result.content[0].text, }, ], }; } catch (error) { logger.error('Error listing Firestore collections:', error); // Create a clean error response return { content: [ { type: 'text', text: JSON.stringify({ error: 'Failed to list Firestore collections', message: error instanceof Error ? error.message : 'Unknown error', }), }, ], }; } } case 'firestore_query_collection_group': { try { logger.debug('Querying Firestore collection group'); // Make sure admin is properly initialized if (!app) { logger.error('Firebase app is not initialized'); return { content: [ { type: 'text', text: JSON.stringify({ error: 'Firebase initialization failed', }), }, ], }; } const collectionId = args.collectionId as string; const limit = Math.min(Math.max(1, (args.limit as number) || 20), 100); // Default 20, max 100 const filters = args.filters as | Array<{ field: string; operator: FirebaseFirestore.WhereFilterOp; value: unknown; }> | undefined; const orderBy = args.orderBy as | Array<{ field: string; direction?: 'asc' | 'desc' }> | undefined; const pageToken = args.pageToken as string | undefined; // Use the Firestore instance directly let query: FirebaseFirestore.Query = admin.firestore().collectionGroup(collectionId); // Apply filters if provided if (filters && filters.length > 0) { filters.forEach(filter => { let filterValue = filter.value; // Check if this might be a timestamp value in ISO format if ( typeof filterValue === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(filterValue) ) { try { // Convert ISO string to Timestamp for Firestore queries filterValue = admin.firestore.Timestamp.fromDate(new Date(filterValue)); } catch { // If conversion fails, use the original value logger.warn( `Failed to convert timestamp string to Timestamp: ${filterValue}` ); } } query = query.where(filter.field, filter.operator, filterValue); }); } // 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 admin.firestore().doc(pageToken).get(); if (lastDoc.exists) { query = query.startAfter(lastDoc); } } catch (error) { logger.error('Invalid pagination token:', error); return { content: [ { type: 'text', text: JSON.stringify({ error: `Invalid pagination token: ${error instanceof Error ? error.message : 'Unknown error'}`, }), }, ], }; } } // Apply limit query = query.limit(limit); // Execute the query const snapshot = await query.get(); // Process the results const documents = snapshot.docs.map(doc => { // For collection groups, we need to use the full path for the URL const fullPath = doc.ref.path; // Handle Timestamp and other Firestore types const data = Object.entries(doc.data()).reduce( (acc, [key, value]) => { // Handle basic types directly if ( typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' || value === null ) { acc[key] = value; } // Convert Date objects to ISO strings else if (value instanceof Date) { acc[key] = value.toISOString(); } // Handle Firestore Timestamp objects properly else if (value instanceof Timestamp) { acc[key] = value.toDate().toISOString(); } // Convert arrays to strings else if (Array.isArray(value)) { acc[key] = value; } // Convert other objects to string representation else if (typeof value === 'object') { acc[key] = '[Object]'; } // Convert other types to strings else { acc[key] = String(value); } return acc; }, {} as Record<string, unknown> ); return { id: doc.id, path: fullPath, data, }; }); // Get the last document for pagination const lastVisible = snapshot.docs[snapshot.docs.length - 1]; const nextPageToken = lastVisible ? lastVisible.ref.path : null; // Create the result object const result = { documents, nextPageToken, }; // Log the response for debugging logger.debug( `Found ${documents.length} documents in collection group ${collectionId}` ); // Ensure we're returning a properly formatted response return { content: [ { type: 'text', text: JSON.stringify(result), }, ], }; } catch (error) { logger.error('Error in collection group query:', error); // Special handling for Firebase index errors const errorMessage = error instanceof Error ? error.message : 'Unknown error'; if ( errorMessage.includes('FAILED_PRECONDITION') && errorMessage.includes('requires an index') ) { const indexUrl = errorMessage.match( /https:\/\/console\.firebase\.google\.com[^\s]*/ )?.[0]; // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify({ error: 'This query requires a composite index.', details: 'When ordering by multiple fields or combining filters with ordering, you need to create a composite index.', indexUrl: indexUrl || null, message: errorMessage, }); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug( 'firestore_query_collection_group index error response:', JSON.stringify(response) ); return response; } // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify({ error: errorMessage, }); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug( 'firestore_query_collection_group error response:', JSON.stringify(response) ); return response; } } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; // Check if it's an index error and extract the index creation URL if ( errorMessage.includes('FAILED_PRECONDITION') && errorMessage.includes('requires an index') ) { const indexUrl = errorMessage.match( /https:\/\/console\.firebase\.google\.com[^\s]*/ )?.[0]; // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify({ error: 'This query requires a composite index.', details: 'When ordering by multiple fields or combining filters with ordering, you need to create a composite index.', indexUrl: indexUrl || null, }); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug('global index error response:', JSON.stringify(response)); return response; } // Ensure clean JSON by parsing and re-stringifying const sanitizedJson = JSON.stringify({ error: errorMessage, }); // Log the response for debugging const response = { content: [ { type: 'text', text: sanitizedJson, }, ], }; logger.debug('global error response:', JSON.stringify(response)); return response; } }); } /** * Starts the MCP server using the configured transport. * This method initializes the appropriate transport based on configuration. */ async run(): Promise<void> { // Wait for Firebase to initialize if (!app) { logger.info('Waiting for Firebase to initialize...'); await new Promise<void>(resolve => { const checkInterval = setInterval(() => { if (app) { clearInterval(checkInterval); resolve(); } }, 100); }); } logger.info( `Starting Firebase MCP server v${config.version} with ${config.transport} transport` ); await initializeTransport(this.server, config); } } // Create and start the server const server = new FirebaseMcpServer(); server.run();

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