Skip to main content
Glama
batch.ts10.9 kB
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { FirestoreClient } from "../firestore/client.js"; import { PermissionManager } from "../permissions/manager.js"; // Type definitions for batch operations const batchWriteOperation = z.discriminatedUnion("type", [ z.object({ type: z.literal("create"), collectionPath: z.string(), documentId: z.string().optional(), data: z.record(z.any()) }), z.object({ type: z.literal("update"), documentPath: z.string(), data: z.record(z.any()) }), z.object({ type: z.literal("delete"), documentPath: z.string() }) ]); const batchReadOperation = z.object({ documentPath: z.string() }); export function registerBatchTools( server: McpServer, firestoreClient: FirestoreClient, permissionManager: PermissionManager ) { // Batch write tool server.tool( "firestore-batch-write", "Execute multiple write operations in a single atomic batch", { operations: z.array(batchWriteOperation).describe("Array of write operations to execute") }, async ({ operations }) => { // Check permissions for all operations for (const operation of operations) { let path: string; let requiredPermission: 'write' | 'delete'; switch (operation.type) { case 'create': path = operation.collectionPath; requiredPermission = 'write'; break; case 'update': path = operation.documentPath; requiredPermission = 'write'; break; case 'delete': path = operation.documentPath; requiredPermission = 'delete'; break; } const pathParts = path.split('/'); const rootCollection = pathParts[0]; if (!permissionManager.hasPermission(rootCollection, requiredPermission)) { return { isError: true, content: [{ type: "text", text: `Access denied for ${operation.type} operation on path: ${path}` }] }; } } try { const batch = firestoreClient.firestore.batch(); const results: any[] = []; for (const operation of operations) { switch (operation.type) { case 'create': { const collectionRef = firestoreClient.firestore.collection(operation.collectionPath); const docRef = operation.documentId ? collectionRef.doc(operation.documentId) : collectionRef.doc(); batch.set(docRef, operation.data); results.push({ type: 'create', id: docRef.id, path: `${operation.collectionPath}/${docRef.id}` }); break; } case 'update': { const docRef = firestoreClient.firestore.doc(operation.documentPath); batch.update(docRef, operation.data); results.push({ type: 'update', path: operation.documentPath }); break; } case 'delete': { const docRef = firestoreClient.firestore.doc(operation.documentPath); batch.delete(docRef); results.push({ type: 'delete', path: operation.documentPath }); break; } } } // Commit the batch await batch.commit(); return { content: [{ type: "text", text: JSON.stringify({ success: true, operationCount: operations.length, operations: results }, null, 2) }] }; } catch (error) { return { isError: true, content: [{ type: "text", text: `Error executing batch write: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); // Batch read tool server.tool( "firestore-batch-read", "Read multiple documents in a single operation", { documentPaths: z.array(z.string()).describe("Array of document paths to read") }, async ({ documentPaths }) => { // Check permissions for all documents for (const documentPath of documentPaths) { const pathParts = documentPath.split('/'); const rootCollection = pathParts[0]; if (!permissionManager.hasPermission(rootCollection, 'read')) { return { isError: true, content: [{ type: "text", text: `Access denied to read document: ${documentPath}` }] }; } } try { const results = await Promise.all( documentPaths.map(async (path) => { const docRef = firestoreClient.firestore.doc(path); const doc = await docRef.get(); return { path, exists: doc.exists, data: doc.exists ? doc.data() : null }; }) ); return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] }; } catch (error) { return { isError: true, content: [{ type: "text", text: `Error executing batch read: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); // Transaction tool server.tool( "firestore-transaction", "Execute a transaction with read and write operations", { reads: z.array(z.string()).describe("Document paths to read in the transaction"), operations: z.array(batchWriteOperation).describe("Write operations to execute based on read data"), conditionScript: z.string().optional().describe("JavaScript condition to evaluate before committing (optional)") }, async ({ reads, operations, conditionScript }) => { // Check permissions for (const readPath of reads) { const pathParts = readPath.split('/'); const rootCollection = pathParts[0]; if (!permissionManager.hasPermission(rootCollection, 'read')) { return { isError: true, content: [{ type: "text", text: `Access denied to read document: ${readPath}` }] }; } } // Check write permissions for (const operation of operations) { let path: string; let requiredPermission: 'write' | 'delete'; switch (operation.type) { case 'create': path = operation.collectionPath; requiredPermission = 'write'; break; case 'update': path = operation.documentPath; requiredPermission = 'write'; break; case 'delete': path = operation.documentPath; requiredPermission = 'delete'; break; } const pathParts = path.split('/'); const rootCollection = pathParts[0]; if (!permissionManager.hasPermission(rootCollection, requiredPermission)) { return { isError: true, content: [{ type: "text", text: `Access denied for ${operation.type} operation on path: ${path}` }] }; } } try { const result = await firestoreClient.firestore.runTransaction(async (transaction) => { // Read phase const readResults: Record<string, any> = {}; for (const readPath of reads) { const docRef = firestoreClient.firestore.doc(readPath); const doc = await transaction.get(docRef); readResults[readPath] = { exists: doc.exists, data: doc.exists ? doc.data() : null }; } // Evaluate condition if provided if (conditionScript) { try { // Create a safe evaluation context const evalCondition = new Function('readResults', conditionScript); const shouldProceed = evalCondition(readResults); if (!shouldProceed) { throw new Error('Transaction condition failed'); } } catch (error) { throw new Error(`Condition evaluation failed: ${error instanceof Error ? error.message : String(error)}`); } } // Write phase const writeResults: any[] = []; for (const operation of operations) { switch (operation.type) { case 'create': { const collectionRef = firestoreClient.firestore.collection(operation.collectionPath); const docRef = operation.documentId ? collectionRef.doc(operation.documentId) : collectionRef.doc(); transaction.set(docRef, operation.data); writeResults.push({ type: 'create', id: docRef.id, path: `${operation.collectionPath}/${docRef.id}` }); break; } case 'update': { const docRef = firestoreClient.firestore.doc(operation.documentPath); transaction.update(docRef, operation.data); writeResults.push({ type: 'update', path: operation.documentPath }); break; } case 'delete': { const docRef = firestoreClient.firestore.doc(operation.documentPath); transaction.delete(docRef); writeResults.push({ type: 'delete', path: operation.documentPath }); break; } } } return { readResults, writeResults }; }); return { content: [{ type: "text", text: JSON.stringify({ success: true, transaction: result }, null, 2) }] }; } catch (error) { return { isError: true, content: [{ type: "text", text: `Error executing transaction: ${error instanceof Error ? error.message : String(error)}` }] }; } } ); }

Latest Blog Posts

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

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