#
tokens: 8506/50000 12/12 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── jest.setup.js
├── package-lock.json
├── package.json
├── README.md
├── src
│   ├── index.ts
│   └── lib
│       └── firebase
│           ├── __tests__
│           │   ├── authClient.test.ts
│           │   ├── firestoreClient.test.ts
│           │   └── storageClient.test.ts
│           ├── authClient.ts
│           ├── firebaseConfig.ts
│           ├── firestoreClient.ts
│           └── storageClient.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Logs
 2 | logs
 3 | *.log
 4 | npm-debug.log*
 5 | yarn-debug.log*
 6 | yarn-error.log*
 7 | lerna-debug.log*
 8 | .pnpm-debug.log*
 9 | 
10 | 
11 | # Compiled binary addons (https://nodejs.org/api/addons.html)
12 | build/
13 | dist/
14 | 
15 | # Dependency directories
16 | node_modules/
17 | jspm_packages/
18 | 
19 | # Snowpack dependency directory (https://snowpack.dev/)
20 | web_modules/
21 | 
22 | # TypeScript cache
23 | *.tsbuildinfo
24 | 
25 | # Optional npm cache directory
26 | .npm
27 | 
28 | # Optional eslint cache
29 | .eslintcache
30 | 
31 | # Optional stylelint cache
32 | .stylelintcache
33 | 
34 | 
35 | # Optional REPL history
36 | .node_repl_history
37 | 
38 | # dotenv environment variable files
39 | .env
40 | .env.local
41 | 
42 | # parcel-bundler cache (https://parceljs.org/)
43 | .cache
44 | .parcel-cache
45 | 
46 | # Next.js build output
47 | .next
48 | out
49 | 
50 | # Nuxt.js build / generate output
51 | .nuxt
52 | dist
53 | 
54 | # Stores VSCode versions used for testing VSCode extensions
55 | .vscode-test
56 | 
57 | # yarn v2
58 | .yarn/cache
59 | .yarn/unplugged
60 | .yarn/build-state.yml
61 | .yarn/install-state.gz
62 | .pnp.*
63 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Firebase MCP Server
 2 | 
 3 | ## Overview
 4 | This is a Firebase MCP (Model Context Protocol) server that provides a unified interface to interact with various Firebase services including Authentication, Firestore, and Storage.
 5 | 
 6 | ## Setup
 7 | 1. Clone and build the project:
 8 |    ```bash
 9 |    git clone https://github.com/gemini-dk/mcp-server-firebase
10 |    cd mcp-server-firebase
11 |    npm install
12 |    npm run build
13 |    ```
14 | 
15 | 2. Get Firebase service account key:
16 |    - Go to Firebase Console > Project Settings > Service accounts
17 |    - Click "Generate new private key"
18 |    - Save the JSON file to your project directory
19 | 
20 | 3. Configure `mcp_settings.json`:
21 |    ```json
22 |    {
23 |      "firebase-mcp": {
24 |        "command": "node",
25 |        "args": [
26 |          "/path/to/mcp-server-firebase/dist/index.js"
27 |        ],
28 |        "env": {
29 |          "SERVICE_ACCOUNT_KEY_PATH": "/path/to/serviceAccountKey.json"
30 |        }
31 |      }
32 |    }
33 |    ```
34 |    Replace `/path/to/mcp-server-firebase` with the actual path where you cloned the repository.
35 |    Replace `/path/to/serviceAccountKey.json` with the path to your service account key file.
36 | 
37 | ## Available APIs
38 | ### Authentication
39 | - Get user by ID or email
40 | 
41 | ### Firestore
42 | - Add/update/delete documents
43 | - List collections/documents
44 | 
45 | ### Storage
46 | - List files in a directory
47 | - Get File metadata and Download URL
48 | 
49 | ## License
50 | - MIT License
51 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2020",
 4 |     "module": "commonjs",
 5 |     "outDir": "./dist",
 6 |     "rootDir": "src",
 7 |     "strict": true,
 8 |     "esModuleInterop": true,
 9 |     "skipLibCheck": true,
10 |     "forceConsistentCasingInFileNames": true
11 |   },
12 |   "include": ["src/**/*.ts"],
13 |   "exclude": ["node_modules", "dist"]
14 | }
15 | 
```

--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------

```javascript
 1 | process.env.SERVICE_ACCOUNT_KEY_PATH = path.resolve(process.cwd(), 'serviceAccountKey.json');
 2 | 
 3 | global.console = {
 4 |   ...console,
 5 |   log: jest.fn((message) => process.stdout.write(message + '\\n')),
 6 |   info: jest.fn((message) => process.stdout.write(message + '\\n')),
 7 |   warn: jest.fn((message) => process.stdout.write(message + '\\n')),
 8 |   error: jest.fn((message) => process.stderr.write(message + '\\n')),
 9 | };
10 | 
```

--------------------------------------------------------------------------------
/src/lib/firebase/__tests__/storageClient.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { listDirectoryFiles, getFileInfo } from '../storageClient';
 2 | 
 3 | const testPath = '';
 4 | const testFilePath = 'testpath';
 5 | 
 6 | describe('listFiles', () => {
 7 |   it('should return a list of files in the specified path', async () => {
 8 |     const result = await listDirectoryFiles(testPath);
 9 |     console.log(result);
10 |   });
11 | });
12 | 
13 | describe('getFileInfo', () => {
14 |   it('should return file metadata and download URL for the specified file', async () => {
15 |     const result = await getFileInfo(testFilePath);
16 |     console.log(result);
17 |   });
18 | });
19 | 
```

--------------------------------------------------------------------------------
/src/lib/firebase/__tests__/authClient.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { getUserByIdOrEmail } from '../authClient';
 2 | 
 3 | const testEmail = '[email protected]';
 4 | const testId = 'testid';
 5 | 
 6 | describe('getUserByIdOrEmail', () => {
 7 |   it('should return user data when a valid UID is provided', async () => {
 8 |     const result = await getUserByIdOrEmail(testId);
 9 |     expect(result.content[0].text).toContain(testId);
10 |     expect(result.content[0].text).toContain(testEmail);
11 |     console.log(result.content[0].text);
12 |   });
13 | 
14 |   it('should return user data when a valid email is provided', async () => {
15 |     const result = await getUserByIdOrEmail(testEmail);
16 |     expect(result.content[0].text).toContain(testId);
17 |     expect(result.content[0].text).toContain(testEmail);
18 |     console.log(result.content[0].text);
19 |   });
20 | });
21 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "firebase-mcp",
 3 |   "version": "1.0.0",
 4 |   "description": "MCP server for Firestore operations",
 5 |   "main": "index.js",
 6 |   "scripts": {
 7 |     "build": "tsc",
 8 |     "test": "jest",
 9 |     "start": "node dist/index.js",
10 |     "dev": "tsc && node dist/index.js"
11 |   },
12 |   "jest": {
13 |     "preset": "ts-jest",
14 |     "testEnvironment": "node",
15 |     "testMatch": [
16 |       "**/__tests__/**/*.ts?(x)",
17 |       "**/?(*.)+(spec|test).ts?(x)"
18 |     ]
19 |   },
20 |   "directories": {
21 |     "test": "test"
22 |   },
23 |   "dependencies": {
24 |     "@modelcontextprotocol/sdk": "^1.5.0",
25 |     "express": "^4.18.2",
26 |     "firebase-admin": "^11.11.1"
27 |   },
28 |   "devDependencies": {
29 |     "@types/express": "^5.0.0",
30 |     "@types/jest": "^27.0.0",
31 |     "@types/node": "^22.13.4",
32 |     "jest": "^29.0.0",
33 |     "ts-jest": "^29.2.5",
34 |     "typescript": "^5.7.3"
35 |   }
36 | }
37 | 
```

--------------------------------------------------------------------------------
/src/lib/firebase/firebaseConfig.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import * as admin from 'firebase-admin';
 2 | import fs from 'fs';
 3 | 
 4 | function initializeFirebase() {
 5 |   const serviceAccountPath = process.env.SERVICE_ACCOUNT_KEY_PATH;
 6 |   
 7 |   if (!serviceAccountPath) {
 8 |     console.error('Firebase initialization skipped: SERVICE_ACCOUNT_KEY_PATH is not set');
 9 |     return null;
10 |   }
11 | 
12 |   try {
13 |     const serviceAccount = require(serviceAccountPath);
14 |     const projectId = getProjectId(serviceAccountPath);
15 | 
16 |     admin.initializeApp({
17 |       credential: admin.credential.cert(serviceAccount),
18 |       storageBucket: `${projectId}.appspot.com`
19 |     });
20 | 
21 |     return admin;
22 |   } catch (error) {
23 |     console.error('Firebase initialization failed:', error);
24 |     return null;
25 |   }
26 | }
27 | 
28 | function getProjectId(serviceAccountPath?: string): string {
29 |   if (!serviceAccountPath) {
30 |     serviceAccountPath = process.env.SERVICE_ACCOUNT_KEY_PATH;
31 |     if (!serviceAccountPath) {
32 |       console.error('Cannot get project ID: SERVICE_ACCOUNT_KEY_PATH is not set');
33 |       return '';
34 |     }
35 |   }
36 |   
37 |   try {
38 |     const serviceAccount = JSON.parse(fs.readFileSync(serviceAccountPath, 'utf8'));
39 |     return serviceAccount.project_id;
40 |   } catch (error) {
41 |     console.error('Failed to get project ID:', error);
42 |     return '';
43 |   }
44 | }
45 | 
46 | const adminApp = initializeFirebase();
47 | const db = adminApp ? admin.firestore() : null;
48 | 
49 | export { db, admin, getProjectId };
50 | 
```

--------------------------------------------------------------------------------
/src/lib/firebase/__tests__/firestoreClient.test.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { listDocuments, addDocument, getDocument, updateDocument, deleteDocument, list_collections } from '../firestoreClient';
 2 | 
 3 | describe('Firebase Client', () => {
 4 |   const collectionName = '';
 5 |   const documentId = 'testid';
 6 | 
 7 | 
 8 | //  it('should add a document', async () => {
 9 | //    const result = await addDocument(collectionName, documentData);
10 | //    console.log(result.content[0].text);
11 | //  });
12 | 
13 |   it('should list collections', async () => {
14 |     const result = await list_collections();
15 |     console.log(result.content[0].text);
16 |   });
17 | 
18 |   it('should list documents', async () => {
19 |     const date = new Date('2025-02-20T00:00:00.000Z');
20 |     const jstDate = new Date(date.toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' }));
21 |     const result = await listDocuments(collectionName, [{ field: 'lastAccessTime', operator: '>=', value: jstDate.toISOString() }]);
22 |     const jsonData = JSON.parse(result.content[0].text);
23 |     const logobj:{totalCount:number,documents:{id:string,url:string}[]} = {totalCount:0,documents:[]};
24 |     logobj.totalCount = jsonData.totalCount;
25 |     for(const doc of jsonData.documents) {
26 |         logobj.documents.push({id:doc.id,url:doc.url});
27 |     }
28 |     console.log(logobj);
29 |   });
30 | 
31 |   it('should get a document', async () => {
32 |     const result = await getDocument(collectionName, documentId);
33 |     console.log(result.content[0].text);
34 |   });
35 | 
36 | //  it('should update a document', async () => {
37 | //    const updatedData = { field1: 'updatedValue' };
38 | //    const result = await updateDocument(collectionName, documentId, updatedData);
39 | //    console.log(result.content[0].text);
40 | //  });
41 | //
42 | //  it('should delete a document', async () => {
43 | //    const result = await deleteDocument(collectionName, documentId);
44 | //    console.log(result.content[0].text);
45 | //  });
46 | });
47 | 
```

--------------------------------------------------------------------------------
/src/lib/firebase/storageClient.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { admin, getProjectId } from './firebaseConfig';
 2 | 
 3 | //const storage = admin.storage().bucket();
 4 | 
 5 | /**
 6 |  * List files in a given path in Firebase Storage.
 7 |  * @param {string} [path] - The optional path to list files from. If not provided, the root is used.
 8 |  * @returns {Promise<{ content: { type: string , text: string }[] }>} - A promise that resolves to an object containing file names.
 9 |  */
10 | export async function listDirectoryFiles(path?: string, pageSize: number = 10, pageToken?: string): Promise<{ content: { type: string , text: string }[] }> {
11 |   const prefix = path ? (path === '' ? '' : (path.endsWith('/') ? path : `${path}/`)) : '';
12 |   const [files, , apiResponse] = await admin.storage().bucket().getFiles({ 
13 |     prefix, 
14 |     delimiter: '/', 
15 |     maxResults: pageSize,
16 |     pageToken
17 |   });
18 |   const nextPageToken = apiResponse.nextPageToken as string || undefined;
19 | 
20 |   const fileNames = await Promise.all(files.map(async (file) => {
21 |     const [signedUrl] = await file.getSignedUrl({
22 |       action: 'read',
23 |       expires: Date.now() + 1000 * 60 * 60 // 1 hour
24 |     });
25 |     return { type: "file", name: file.name, downloadURL: signedUrl };
26 |   }));
27 | 
28 |   const projectId = getProjectId();
29 |   const bucketName = admin.storage().bucket().name;
30 |   const directoryNames = (apiResponse.prefixes || []).map((prefix:string) => {    
31 |     //const encodedPrefix = encodeURIComponent(prefix).replace(/'/g, '%27').replace(/%20/g, '+');
32 |     const tmpPrefix = prefix.replace(/\/$/, '');
33 |     const encodedPrefix = `~2F${tmpPrefix.replace(/\//g, '~2F')}`;
34 |     const consoleUrl = `https://console.firebase.google.com/project/${projectId}/storage/${bucketName}/files/${encodedPrefix}`;
35 |     return { type: "directory", name: prefix, url: consoleUrl };
36 |   });
37 | 
38 |     const result = { 
39 |       nextPageToken: nextPageToken, 
40 |       files: [...fileNames, ...directoryNames],
41 |       hasMore: nextPageToken !== undefined
42 |     };
43 |   return {
44 |     content:[
45 |       {
46 |         type: "text",
47 |         text: JSON.stringify(result,null,2)
48 |       }
49 |     ]
50 |   }
51 | }
52 | 
53 | /**
54 |  * Get file information including metadata and download URL.
55 |  * @param {string} filePath - The path of the file to get information for.
56 |  * @returns {Promise<{ content: object }>} - A promise that resolves to an object containing file metadata and download URL.
57 |  */
58 | export async function getFileInfo(filePath: string): Promise<{ content: { type: string , text: string }[] }> {
59 |   const file = admin.storage().bucket().file(filePath);
60 |   const [metadata] = await file.getMetadata();
61 |   const [url] = await file.getSignedUrl({
62 |     action: 'read',
63 |     expires: Date.now() + 1000 * 60 * 60 // 1 hour
64 |   });
65 |   const result = { metadata, downloadUrl:url };
66 |   return {
67 |     content: [
68 |       { type:'text', text: JSON.stringify(result,null,2) }
69 |     ]
70 |   };
71 | }
72 | 
```

--------------------------------------------------------------------------------
/src/lib/firebase/firestoreClient.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Query, Timestamp } from 'firebase-admin/firestore';
  2 | import {db, getProjectId} from './firebaseConfig';
  3 | import fs from 'fs';
  4 | import path from 'path';
  5 | 
  6 | export async function list_collections(documentPath?: string, limit: number = 20, pageToken?: string) {
  7 |   try {
  8 |     if (!db) {
  9 |       return { content: [{ type: 'text', text: 'Firebase is not initialized. SERVICE_ACCOUNT_KEY_PATH environment variable is required.' }], isError: true };
 10 |     }
 11 |     
 12 |     let collections;
 13 |     if (documentPath) {
 14 |       const docRef = db.doc(documentPath);
 15 |       collections = await docRef.listCollections();
 16 |     } else {
 17 |       collections = await db.listCollections();
 18 |     }
 19 |     
 20 |     // Sort collections by name
 21 |     collections.sort((a, b) => a.id.localeCompare(b.id));
 22 |     
 23 |     // Find start index
 24 |     const startIndex = pageToken ? collections.findIndex(c => c.id === pageToken) + 1 : 0;
 25 |     
 26 |     // Apply limit
 27 |     const paginatedCollections = collections.slice(startIndex, startIndex + limit);
 28 |     
 29 |     const projectId = getProjectId();
 30 |     const collectionData = paginatedCollections.map((collection) => {
 31 |       const collectionUrl = `https://console.firebase.google.com/project/${projectId}/firestore/data/${documentPath}/${collection.id}`;
 32 |       return { name: collection.id, url: collectionUrl };
 33 |     });
 34 |     
 35 |     return { 
 36 |       content: [{
 37 |         type: 'text', 
 38 |         text: JSON.stringify({
 39 |           collections: collectionData,
 40 |           nextPageToken: collections.length > startIndex + limit ? 
 41 |             paginatedCollections[paginatedCollections.length - 1].id : null,
 42 |           hasMore: collections.length > startIndex + limit
 43 |         })
 44 |       }]
 45 |     };
 46 |   } catch (error) {
 47 |     return { content: [{ type: 'text', text: `Error listing collections: ${(error as Error).message}` }], isError: true };
 48 |   }
 49 | }
 50 | 
 51 | function convertTimestampsToISO(data: any) {
 52 |   for (const key in data) {
 53 |     if (data[key] instanceof Timestamp) {
 54 |       data[key] = data[key].toDate().toISOString();
 55 |     }
 56 |   }
 57 |   return data;
 58 | }
 59 | 
 60 | 
 61 | export async function listDocuments(collection: string, filters: Array<{ field: string, operator: FirebaseFirestore.WhereFilterOp, value: any }> = [], limit: number = 20, pageToken?: string) {
 62 |   const projectId = getProjectId();
 63 |   try {
 64 |     if (!db) {
 65 |       return { content: [{ type: 'text', text: 'Firebase is not initialized. SERVICE_ACCOUNT_KEY_PATH environment variable is required.' }], isError: true };
 66 |     }
 67 |     
 68 |     const collectionRef = db.collection(collection);
 69 |     let filteredQuery: Query = collectionRef;
 70 |     for (const filter of filters) {
 71 |       let filterValue = filter.value;
 72 |       if (typeof filterValue === 'string' && !isNaN(Date.parse(filterValue))) {
 73 |         filterValue = Timestamp.fromDate(new Date(filterValue));
 74 |       }
 75 |       filteredQuery = filteredQuery.where(filter.field, filter.operator, filterValue);
 76 |     }
 77 |     
 78 |     // Apply pageToken if provided
 79 |     if (pageToken) {
 80 |       const startAfterDoc = await collectionRef.doc(pageToken).get();
 81 |       filteredQuery = filteredQuery.startAfter(startAfterDoc);
 82 |     }
 83 | 
 84 |     // Get total count of documents matching the filter
 85 |     const countSnapshot = await filteredQuery.get();
 86 |     const totalCount = countSnapshot.size;
 87 | 
 88 |     // Get the first 'limit' documents
 89 |     const limitedQuery = filteredQuery.limit(limit);
 90 |     const snapshot = await limitedQuery.get();
 91 | 
 92 |     if (snapshot.empty) {
 93 |       return { content: [{ type: 'text', text: 'No matching documents found' }], isError: true };
 94 |     }
 95 |     
 96 |     const documents = snapshot.docs.map((doc: any) => {
 97 |       const data = doc.data();
 98 |       convertTimestampsToISO(data);
 99 |       const consoleUrl = `https://console.firebase.google.com/project/${projectId}/firestore/data/${collection}/${doc.id}`;
100 |       return { id: doc.id, url: consoleUrl, document: data };
101 |     });
102 |     
103 |     return { 
104 |       content: [{
105 |         type: 'text', 
106 |         text: JSON.stringify({
107 |           totalCount,
108 |           documents,
109 |           pageToken: documents.length > 0 ? documents[documents.length - 1].id : null,
110 |           hasMore: totalCount > limit
111 |         })
112 |       }]
113 |     };
114 |   } catch (error) {
115 |     return { content: [{ type: 'text', text: `Error listing documents: ${(error as Error).message}` }], isError: true };
116 |   }
117 | }
118 | 
119 | export async function addDocument(collection: string, data: any) {
120 |   try {
121 |     if (!db) {
122 |       return { content: [{ type: 'text', text: 'Firebase is not initialized. SERVICE_ACCOUNT_KEY_PATH environment variable is required.' }], isError: true };
123 |     }
124 |     
125 |     const docRef = await db.collection(collection).add(data);
126 |     const projectId = getProjectId();
127 |     convertTimestampsToISO(data);
128 |     const consoleUrl = `https://console.firebase.google.com/project/${projectId}/firestore/data/${collection}/${docRef.id}`;
129 |     return { content: [{ type: 'text', text: JSON.stringify({ id: docRef.id, url: consoleUrl, document: data }) }] };
130 |   } catch (error) {
131 |     return { content: [{ type: 'text', text: `Error adding document: ${(error as Error).message}` }], isError: true };
132 |   }
133 | }
134 | 
135 | export async function getDocument(collection: string, id: string) {
136 |   try {
137 |     if (!db) {
138 |       return { content: [{ type: 'text', text: 'Firebase is not initialized. SERVICE_ACCOUNT_KEY_PATH environment variable is required.' }], isError: true };
139 |     }
140 |     
141 |     const doc = await db.collection(collection).doc(id).get();
142 |     if (!doc.exists) {
143 |       return { content: [{ type: 'text', text: 'Document not found' }], isError: true };
144 |     }
145 |     const projectId = getProjectId();
146 |     const data = doc.data();
147 |     convertTimestampsToISO(data);
148 |     const consoleUrl = `https://console.firebase.google.com/project/${projectId}/firestore/data/${collection}/${id}`;
149 |     return { content: [{ type: 'text', text: JSON.stringify({ id, url: consoleUrl, document: data }) }] };
150 |   } catch (error) {
151 |     return { content: [{ type: 'text', text: `Error getting document: ${(error as Error).message}` }], isError: true };
152 |   }
153 | }
154 | 
155 | export async function updateDocument(collection: string, id: string, data: any) {
156 |   try {
157 |     if (!db) {
158 |       return { content: [{ type: 'text', text: 'Firebase is not initialized. SERVICE_ACCOUNT_KEY_PATH environment variable is required.' }], isError: true };
159 |     }
160 |     
161 |     await db.collection(collection).doc(id).update(data);
162 |     const projectId = getProjectId();
163 |     convertTimestampsToISO(data);
164 |     const consoleUrl = `https://console.firebase.google.com/project/${projectId}/firestore/data/${collection}/${id}`;
165 |     return { content: [{ type: 'text', text: JSON.stringify({ id, url: consoleUrl, document: data }) }] };
166 |   } catch (error) {
167 |     return { content: [{ type: 'text', text: `Error updating document: ${(error as Error).message}` }], isError: true };
168 |   }
169 | }
170 | 
171 | export async function deleteDocument(collection: string, id: string) {
172 |   try {
173 |     if (!db) {
174 |       return { content: [{ type: 'text', text: 'Firebase is not initialized. SERVICE_ACCOUNT_KEY_PATH environment variable is required.' }], isError: true };
175 |     }
176 |     
177 |     await db.collection(collection).doc(id).delete();
178 |     return { content: [{ type: 'text', text: 'Document deleted successfully' }] };
179 |   } catch (error) {
180 |     return { content: [{ type: 'text', text: `Error deleting document: ${(error as Error).message}` }], isError: true };
181 |   }
182 | }
183 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
  2 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
  3 | import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
  4 | import { addDocument, getDocument, updateDocument, deleteDocument, listDocuments, list_collections } from './lib/firebase/firestoreClient';
  5 | import { db } from './lib/firebase/firebaseConfig';
  6 | import { listDirectoryFiles, getFileInfo } from './lib/firebase/storageClient';
  7 | import { getUserByIdOrEmail } from './lib/firebase/authClient';
  8 | 
  9 | class FirebaseMcpServer {
 10 |   private server: Server;
 11 | 
 12 |   constructor() {
 13 |     this.server = new Server(
 14 |       {
 15 |         name: 'firebase-mcp-server',
 16 |         version: '0.9.0'
 17 |       },
 18 |       {
 19 |         capabilities: {
 20 |           tools: {}
 21 |         }
 22 |       }
 23 |     );
 24 | 
 25 |     this.setupToolHandlers();
 26 | 
 27 |     this.server.onerror = (error) => console.error('[MCP Error]', error);
 28 |     process.on('SIGINT', async () => {
 29 |       await this.server.close();
 30 |       process.exit(0);
 31 |     });
 32 |   }
 33 | 
 34 |   private setupToolHandlers() {
 35 |     this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
 36 |       tools: [
 37 |         {
 38 |           name: 'firestore_add_document',
 39 |           description: 'Add a document to a Firestore collection',
 40 |           inputSchema: {
 41 |             type: 'object',
 42 |             properties: {
 43 |               collection: {
 44 |                 type: 'string',
 45 |                 description: 'Collection name'
 46 |               },
 47 |               data: {
 48 |                 type: 'object',
 49 |                 description: 'Document data'
 50 |               }
 51 |             },
 52 |             required: ['collection', 'data']
 53 |           }
 54 |         },
 55 |         {
 56 |           name: 'firestore_list_collections',
 57 |           description: 'List collections in Firestore. If documentPath is provided, returns subcollections under that document; otherwise returns root collections.',
 58 |           inputSchema: {
 59 |             type: 'object',
 60 |             properties: {
 61 |             documentPath: {
 62 |               type: 'string',
 63 |               description: 'Optional parent document path'
 64 |             },
 65 |             limit: {
 66 |               type: 'number',
 67 |               description: 'Number of collections to return',
 68 |               default: 20
 69 |             },
 70 |             pageToken: {
 71 |               type: 'string',
 72 |               description: 'Token for pagination to get the next page of results'
 73 |             }
 74 |             },
 75 |             required: []
 76 |           }
 77 |         },
 78 |         {
 79 |           name: 'firestore_list_documents',
 80 |           description: 'List documents from a Firestore collection with optional filtering',
 81 |           inputSchema: {
 82 |             type: 'object',
 83 |             properties: {
 84 |               collection: {
 85 |                 type: 'string',
 86 |                 description: 'Collection name'
 87 |               },
 88 |               filters: {
 89 |                 type: 'array',
 90 |                 description: 'Array of filter conditions',
 91 |                 items: {
 92 |                   type: 'object',
 93 |                   properties: {
 94 |                     field: {
 95 |                       type: 'string',
 96 |                       description: 'Field name to filter'
 97 |                     },
 98 |                     operator: {
 99 |                       type: 'string',
100 |                       description: 'Comparison operator'
101 |                     },
102 |                     value: {
103 |                       type: 'any',
104 |                       description: 'Value to compare against (use ISO format for dates)'
105 |                     }
106 |                   },
107 |                   required: ['field', 'operator', 'value']
108 |                 }
109 |               },
110 |             limit: {
111 |               type: 'number',
112 |               description: 'Number of documents to return',
113 |               default: 20
114 |             },
115 |             pageToken: {
116 |               type: 'string',
117 |               description: 'Token for pagination to get the next page of results'
118 |             }
119 |             },
120 |             required: ['collection']
121 |           }
122 |         },
123 |         {
124 |           name: 'firestore_get_document',
125 |           description: 'Get a document from a Firestore collection',
126 |           inputSchema: {
127 |             type: 'object',
128 |             properties: {
129 |               collection: {
130 |                 type: 'string',
131 |                 description: 'Collection name'
132 |               },
133 |               id: {
134 |                 type: 'string',
135 |                 description: 'Document ID'
136 |               }
137 |             },
138 |             required: ['collection', 'id']
139 |           }
140 |         },
141 |         {
142 |           name: 'firestore_update_document',
143 |           description: 'Update a document in a Firestore collection',
144 |           inputSchema: {
145 |             type: 'object',
146 |             properties: {
147 |               collection: {
148 |                 type: 'string',
149 |                 description: 'Collection name'
150 |               },
151 |               id: {
152 |                 type: 'string',
153 |                 description: 'Document ID'
154 |               },
155 |               data: {
156 |                 type: 'object',
157 |                 description: 'Updated document data'
158 |               }
159 |             },
160 |             required: ['collection', 'id', 'data']
161 |           }
162 |         },
163 |         {
164 |           name: 'firestore_delete_document',
165 |           description: 'Delete a document from a Firestore collection',
166 |           inputSchema: {
167 |             type: 'object',
168 |             properties: {
169 |               collection: {
170 |                 type: 'string',
171 |                 description: 'Collection name'
172 |               },
173 |               id: {
174 |                 type: 'string',
175 |                 description: 'Document ID'
176 |               }
177 |             },
178 |             required: ['collection', 'id']
179 |           }
180 |         },
181 |         {
182 |           name: "auth_get_user",
183 |           description: "Get a user by ID or email from Firebase Authentication",
184 |           inputSchema: {
185 |             type: "object",
186 |             properties: {
187 |               identifier: {
188 |                 type: "string",
189 |                 description: "User ID or email address"
190 |               }
191 |             },
192 |             required: ["identifier"]
193 |           }
194 |         },
195 |         {
196 |           "name": "storage_list_files",
197 |           "description": "List files in a given path in Firebase Storage",
198 |           "inputSchema": {
199 |             "type": "object",
200 |             "properties": {
201 |               "directoryPath": {
202 |                 "type": "string",
203 |                 "description": "The optional path to list files from. If not provided, the root is used."
204 |               }
205 |             },
206 |             "required": []
207 |           }
208 |         },
209 |         {
210 |           "name": "storage_get_file_info",
211 |           "description": "Get file information including metadata and download URL",
212 |           "inputSchema": {
213 |             "type": "object",
214 |             "properties": {
215 |               "filePath": {
216 |                 "type": "string",
217 |                 "description": "The path of the file to get information for"
218 |               }
219 |             },
220 |             "required": ["filePath"]
221 |           }
222 |         }
223 |         ]
224 |     }));
225 | 
226 |     this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
227 |       const { name, arguments: args = {} } = request.params;
228 |       switch (name) {
229 |         case 'firestore_add_document':
230 |           return addDocument(args.collection as string, args.data as object);
231 |         case 'firestore_list_documents':
232 |           return listDocuments(
233 |             args.collection as string,
234 |             args.filters as Array<{ field: string, operator: FirebaseFirestore.WhereFilterOp, value: any }>,
235 |             args.limit as number,
236 |             args.pageToken as string | undefined
237 |           );
238 |         case 'firestore_get_document':
239 |           return getDocument(args.collection as string, args.id as string);
240 |         case 'firestore_update_document':
241 |           return updateDocument(args.collection as string, args.id as string, args.data as object);
242 |         case 'firestore_delete_document':
243 |           return deleteDocument(args.collection as string, args.id as string);
244 |         case 'firestore_list_collections':
245 |           return list_collections(
246 |             args.documentPath as string | undefined,
247 |             args.limit as number | undefined,
248 |             args.pageToken as string | undefined
249 |           );
250 |         case 'auth_get_user':
251 |           return getUserByIdOrEmail(args.identifier as string);
252 |         case 'storage_list_files':
253 |           return listDirectoryFiles(
254 |             args.directoryPath as string | undefined,
255 |             args.pageSize as number | undefined,
256 |             args.pageToken as string | undefined
257 |           );
258 |         case 'storage_get_file_info':
259 |           return getFileInfo(args.filePath as string);
260 |         default:
261 |           throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
262 |       }
263 |     });
264 |   }
265 | 
266 |   async run() {
267 |     const transport = new StdioServerTransport();
268 |     await this.server.connect(transport);
269 |     console.error('Firebase MCP server running on stdio');
270 |   }
271 | }
272 | 
273 | const server = new FirebaseMcpServer();
274 | server.run().catch(console.error);
275 | 
```