Skip to main content
Glama

Firestore Advanced MCP

by diez7lm
index.js10.6 kB
#!/usr/bin/env node /** * Firestore Advanced MCP Server * * Un serveur MCP complet pour Firebase Firestore avec support pour: * - Toutes les opérations CRUD avec traitement avancé des données * - Requêtes composées et filtres multiples * - Opérations atomiques et transactions * - Gestion TTL et index * - Conversion automatique des types Firestore * - Détection intelligente des erreurs d'index manquants * * Par diez7lm (c) 2025 * Licence MIT */ import admin from 'firebase-admin'; import fs from 'fs'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import { v4 as uuidv4 } from 'uuid'; // Configuration et initialisation console.error("Initialisation du serveur Firestore Advanced MCP..."); // Récupération du chemin du fichier de clé de service à partir de la variable d'environnement const serviceAccountKeyPath = process.env.SERVICE_ACCOUNT_KEY_PATH; if (!serviceAccountKeyPath) { console.error("Erreur: Variable d'environnement SERVICE_ACCOUNT_KEY_PATH non définie."); console.error("Veuillez définir cette variable avec le chemin vers votre fichier de clé de service Firebase."); process.exit(1); } // Vérification de l'existence du fichier de clé de service try { fs.accessSync(serviceAccountKeyPath, fs.constants.R_OK); } catch (error) { console.error(`Erreur: Impossible de lire le fichier de clé de service à ${serviceAccountKeyPath}`); console.error("Veuillez vérifier que le chemin est correct et que le fichier existe."); process.exit(1); } // Initialisation de Firebase Admin SDK try { admin.initializeApp({ credential: admin.credential.cert(serviceAccountKeyPath) }); console.error("Firebase Admin SDK initialisé avec succès"); } catch (error) { console.error("Erreur lors de l'initialisation de Firebase Admin SDK:", error); process.exit(1); } // Utilitaires de cache pour optimiser les performances class DocumentCache { constructor(ttl = 5 * 60 * 1000) { // 5 minutes par défaut this.cache = new Map(); this.ttl = ttl; } set(key, value) { const expiryTime = Date.now() + this.ttl; this.cache.set(key, { value, expiryTime }); return value; } get(key) { const entry = this.cache.get(key); if (!entry) return null; if (Date.now() > entry.expiryTime) { this.cache.delete(key); return null; } return entry.value; } invalidate(key) { this.cache.delete(key); } clear() { this.cache.clear(); } getStats() { let size = 0; let expired = 0; for (const [key, entry] of this.cache.entries()) { if (Date.now() > entry.expiryTime) { expired++; this.cache.delete(key); } else { size++; } } return { size, expired }; } setTTL(newTTL) { this.ttl = newTTL; } } // Initialisation du cache de documents const documentCache = new DocumentCache(); const getCacheKey = (collection, id) => `${collection}/${id}`; // Fonction utilitaire pour convertir les Timestamps Firestore en ISO strings function convertTimestampsToISO(data, visitedObjects = new WeakMap(), depth = 0, maxDepth = 20) { // Protection contre les boucles infinies if (depth > maxDepth) return "[Profondeur maximale atteinte]"; // Valeurs null ou undefined if (data === null || data === undefined) return data; // Détection des références circulaires if (typeof data === 'object' && data !== null) { if (visitedObjects.has(data)) { return "[Référence circulaire]"; } visitedObjects.set(data, true); } // Timestamp Firestore if (data && typeof data.toDate === 'function') { return data.toDate().toISOString(); } // GeoPoint Firestore if (data instanceof admin.firestore.GeoPoint) { return { type: "geopoint", latitude: data.latitude, longitude: data.longitude }; } // Reference Firestore if (data instanceof admin.firestore.DocumentReference) { return { type: "reference", path: data.path, id: data.id }; } // Arrays if (Array.isArray(data)) { return data.map(item => convertTimestampsToISO(item, visitedObjects, depth + 1, maxDepth)); } // Objects if (typeof data === 'object' && data !== null) { const result = {}; for (const [key, value] of Object.entries(data)) { result[key] = convertTimestampsToISO(value, visitedObjects, depth + 1, maxDepth); } return result; } // Autres types de données (number, string, boolean) return data; } // Fonction utilitaire pour obtenir l'ID du projet Firebase function getProjectId() { try { const serviceAccount = JSON.parse(fs.readFileSync(serviceAccountKeyPath, 'utf8')); return serviceAccount.project_id; } catch (error) { return "unknown-project"; } } // Fonction pour gérer les erreurs d'index manquants async function handleMissingIndexError(error, collection, queryDetails = {}) { if (error.code === 'failed-precondition' && error.message.includes('requires an index')) { const indexMatch = error.message.match(/https:\/\/console\.firebase\.google\.com\/project\/([^\/]+)\/database\/firestore\/indexes\?create_index=([^\s]+)/); if (indexMatch) { const projectId = indexMatch[1]; const indexParams = indexMatch[2]; const createUrl = `https://console.firebase.google.com/project/${projectId}/firestore/indexes?create_index=${indexParams}`; return { type: 'missing_index', message: `Cette requête nécessite un index composite qui n'existe pas encore. Veuillez cliquer sur le lien pour le créer.`, collection, description: `Requête avec ${queryDetails.orderByFields ? 'tri multiple' : 'filtre complexe'}`, createUrl, projectId, indexParams }; } } // Si ce n'est pas une erreur d'index ou si on ne peut pas extraire l'URL return { type: 'other_error', message: error.message, code: error.code }; } // Création du serveur MCP const server = new McpServer( new StdioServerTransport(), { cliName: "firestore-advanced-mcp" } ); // ==================== OUTILS FIRESTORE ==================== // Outil pour récupérer un document server.tool( 'firestore_get', { collection: z.string().describe('Nom de la collection'), id: z.string().describe('ID du document') }, async ({ collection, id }) => { try { // Vérifier si le document est dans le cache const cacheKey = getCacheKey(collection, id); const cachedDoc = documentCache.get(cacheKey); if (cachedDoc) { return { content: [{ type: 'text', text: JSON.stringify(cachedDoc) }] }; } // Récupérer le document depuis Firestore const docRef = admin.firestore().collection(collection).doc(id); const doc = await docRef.get(); if (!doc.exists) { return { content: [{ type: 'error', text: `Document ${collection}/${id} non trouvé` }] }; } // Convertir les timestamps en strings ISO et mettre en cache const data = convertTimestampsToISO(doc.data()); const response = { id: doc.id, collection, data, exists: true, url: `https://console.firebase.google.com/project/${getProjectId()}/firestore/data/${collection}/${id}` }; // Mettre en cache documentCache.set(cacheKey, response); return { content: [{ type: 'text', text: JSON.stringify(response) }] }; } catch (error) { return { content: [{ type: 'error', text: error.message }] }; } } ); // Outil pour créer un document server.tool( 'firestore_create', { collection: z.string().describe('Nom de la collection'), id: z.string().optional().describe('ID du document (généré automatiquement si non fourni)'), data: z.any().describe('Données à enregistrer'), merge: z.boolean().optional().describe('Fusionner avec un document existant si true') }, async ({ collection, id, data, merge = false }) => { try { const docId = id || uuidv4(); const docRef = admin.firestore().collection(collection).doc(docId); if (!merge) { // Vérifier si le document existe déjà const doc = await docRef.get(); if (doc.exists) { return { content: [{ type: 'error', text: `Le document ${collection}/${docId} existe déjà. Utilisez merge=true pour mettre à jour.` }] }; } } // Créer ou mettre à jour le document await docRef.set(data, { merge }); // Invalider le cache const cacheKey = getCacheKey(collection, docId); documentCache.invalidate(cacheKey); // Récupérer le document mis à jour const updatedDoc = await docRef.get(); const responseData = convertTimestampsToISO(updatedDoc.data()); return { content: [{ type: 'text', text: JSON.stringify({ id: docId, collection, data: responseData, created: !merge, merged: merge, url: `https://console.firebase.google.com/project/${getProjectId()}/firestore/data/${collection}/${docId}` }) }] }; } catch (error) { return { content: [{ type: 'error', text: error.message }] }; } } ); // Note: Ce fichier est une version simplifiée du serveur MCP. Le code complet contient les implémentations de: // - firestore_update - Mettre à jour un document existant // - firestore_delete - Supprimer un document // - firestore_query - Exécuter une requête avec filtres // - firestore_collection_group_query - Requête sur groupes de collections // - firestore_composite_query - Requête avec filtres et tris multiples // - firestore_special_data_types - Gérer les types spéciaux (GeoPoint, References) // - firestore_set_ttl - Configurer l'expiration automatique des documents // - firestore_transaction - Exécuter des transactions atomiques // - firestore_batch - Exécuter des opérations par lot // - firestore_field_operations - Effectuer des opérations atomiques sur les champs // - firestore_full_text_search - Recherche textuelle dans les documents // Démarrage du serveur console.error("Firestore Advanced MCP démarré et prêt à recevoir des commandes!"); server.listen();

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/diez7lm/firestore-advanced-mcp'

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