# 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:
--------------------------------------------------------------------------------
```
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/
dist/
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Optional REPL history
.node_repl_history
# dotenv environment variable files
.env
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
# Firebase MCP Server
## Overview
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.
## Setup
1. Clone and build the project:
```bash
git clone https://github.com/gemini-dk/mcp-server-firebase
cd mcp-server-firebase
npm install
npm run build
```
2. Get Firebase service account key:
- Go to Firebase Console > Project Settings > Service accounts
- Click "Generate new private key"
- Save the JSON file to your project directory
3. Configure `mcp_settings.json`:
```json
{
"firebase-mcp": {
"command": "node",
"args": [
"/path/to/mcp-server-firebase/dist/index.js"
],
"env": {
"SERVICE_ACCOUNT_KEY_PATH": "/path/to/serviceAccountKey.json"
}
}
}
```
Replace `/path/to/mcp-server-firebase` with the actual path where you cloned the repository.
Replace `/path/to/serviceAccountKey.json` with the path to your service account key file.
## Available APIs
### Authentication
- Get user by ID or email
### Firestore
- Add/update/delete documents
- List collections/documents
### Storage
- List files in a directory
- Get File metadata and Download URL
## License
- MIT License
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
```
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
```javascript
process.env.SERVICE_ACCOUNT_KEY_PATH = path.resolve(process.cwd(), 'serviceAccountKey.json');
global.console = {
...console,
log: jest.fn((message) => process.stdout.write(message + '\\n')),
info: jest.fn((message) => process.stdout.write(message + '\\n')),
warn: jest.fn((message) => process.stdout.write(message + '\\n')),
error: jest.fn((message) => process.stderr.write(message + '\\n')),
};
```
--------------------------------------------------------------------------------
/src/lib/firebase/__tests__/storageClient.test.ts:
--------------------------------------------------------------------------------
```typescript
import { listDirectoryFiles, getFileInfo } from '../storageClient';
const testPath = '';
const testFilePath = 'testpath';
describe('listFiles', () => {
it('should return a list of files in the specified path', async () => {
const result = await listDirectoryFiles(testPath);
console.log(result);
});
});
describe('getFileInfo', () => {
it('should return file metadata and download URL for the specified file', async () => {
const result = await getFileInfo(testFilePath);
console.log(result);
});
});
```
--------------------------------------------------------------------------------
/src/lib/firebase/__tests__/authClient.test.ts:
--------------------------------------------------------------------------------
```typescript
import { getUserByIdOrEmail } from '../authClient';
const testEmail = '[email protected]';
const testId = 'testid';
describe('getUserByIdOrEmail', () => {
it('should return user data when a valid UID is provided', async () => {
const result = await getUserByIdOrEmail(testId);
expect(result.content[0].text).toContain(testId);
expect(result.content[0].text).toContain(testEmail);
console.log(result.content[0].text);
});
it('should return user data when a valid email is provided', async () => {
const result = await getUserByIdOrEmail(testEmail);
expect(result.content[0].text).toContain(testId);
expect(result.content[0].text).toContain(testEmail);
console.log(result.content[0].text);
});
});
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
{
"name": "firebase-mcp",
"version": "1.0.0",
"description": "MCP server for Firestore operations",
"main": "index.js",
"scripts": {
"build": "tsc",
"test": "jest",
"start": "node dist/index.js",
"dev": "tsc && node dist/index.js"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"testMatch": [
"**/__tests__/**/*.ts?(x)",
"**/?(*.)+(spec|test).ts?(x)"
]
},
"directories": {
"test": "test"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.5.0",
"express": "^4.18.2",
"firebase-admin": "^11.11.1"
},
"devDependencies": {
"@types/express": "^5.0.0",
"@types/jest": "^27.0.0",
"@types/node": "^22.13.4",
"jest": "^29.0.0",
"ts-jest": "^29.2.5",
"typescript": "^5.7.3"
}
}
```
--------------------------------------------------------------------------------
/src/lib/firebase/firebaseConfig.ts:
--------------------------------------------------------------------------------
```typescript
import * as admin from 'firebase-admin';
import fs from 'fs';
function initializeFirebase() {
const serviceAccountPath = process.env.SERVICE_ACCOUNT_KEY_PATH;
if (!serviceAccountPath) {
console.error('Firebase initialization skipped: SERVICE_ACCOUNT_KEY_PATH is not set');
return null;
}
try {
const serviceAccount = require(serviceAccountPath);
const projectId = getProjectId(serviceAccountPath);
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
storageBucket: `${projectId}.appspot.com`
});
return admin;
} catch (error) {
console.error('Firebase initialization failed:', error);
return null;
}
}
function getProjectId(serviceAccountPath?: string): string {
if (!serviceAccountPath) {
serviceAccountPath = process.env.SERVICE_ACCOUNT_KEY_PATH;
if (!serviceAccountPath) {
console.error('Cannot get project ID: SERVICE_ACCOUNT_KEY_PATH is not set');
return '';
}
}
try {
const serviceAccount = JSON.parse(fs.readFileSync(serviceAccountPath, 'utf8'));
return serviceAccount.project_id;
} catch (error) {
console.error('Failed to get project ID:', error);
return '';
}
}
const adminApp = initializeFirebase();
const db = adminApp ? admin.firestore() : null;
export { db, admin, getProjectId };
```
--------------------------------------------------------------------------------
/src/lib/firebase/__tests__/firestoreClient.test.ts:
--------------------------------------------------------------------------------
```typescript
import { listDocuments, addDocument, getDocument, updateDocument, deleteDocument, list_collections } from '../firestoreClient';
describe('Firebase Client', () => {
const collectionName = '';
const documentId = 'testid';
// it('should add a document', async () => {
// const result = await addDocument(collectionName, documentData);
// console.log(result.content[0].text);
// });
it('should list collections', async () => {
const result = await list_collections();
console.log(result.content[0].text);
});
it('should list documents', async () => {
const date = new Date('2025-02-20T00:00:00.000Z');
const jstDate = new Date(date.toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' }));
const result = await listDocuments(collectionName, [{ field: 'lastAccessTime', operator: '>=', value: jstDate.toISOString() }]);
const jsonData = JSON.parse(result.content[0].text);
const logobj:{totalCount:number,documents:{id:string,url:string}[]} = {totalCount:0,documents:[]};
logobj.totalCount = jsonData.totalCount;
for(const doc of jsonData.documents) {
logobj.documents.push({id:doc.id,url:doc.url});
}
console.log(logobj);
});
it('should get a document', async () => {
const result = await getDocument(collectionName, documentId);
console.log(result.content[0].text);
});
// it('should update a document', async () => {
// const updatedData = { field1: 'updatedValue' };
// const result = await updateDocument(collectionName, documentId, updatedData);
// console.log(result.content[0].text);
// });
//
// it('should delete a document', async () => {
// const result = await deleteDocument(collectionName, documentId);
// console.log(result.content[0].text);
// });
});
```
--------------------------------------------------------------------------------
/src/lib/firebase/storageClient.ts:
--------------------------------------------------------------------------------
```typescript
import { admin, getProjectId } from './firebaseConfig';
//const storage = admin.storage().bucket();
/**
* List files in a given path in Firebase Storage.
* @param {string} [path] - The optional path to list files from. If not provided, the root is used.
* @returns {Promise<{ content: { type: string , text: string }[] }>} - A promise that resolves to an object containing file names.
*/
export async function listDirectoryFiles(path?: string, pageSize: number = 10, pageToken?: string): Promise<{ content: { type: string , text: string }[] }> {
const prefix = path ? (path === '' ? '' : (path.endsWith('/') ? path : `${path}/`)) : '';
const [files, , apiResponse] = await admin.storage().bucket().getFiles({
prefix,
delimiter: '/',
maxResults: pageSize,
pageToken
});
const nextPageToken = apiResponse.nextPageToken as string || undefined;
const fileNames = await Promise.all(files.map(async (file) => {
const [signedUrl] = await file.getSignedUrl({
action: 'read',
expires: Date.now() + 1000 * 60 * 60 // 1 hour
});
return { type: "file", name: file.name, downloadURL: signedUrl };
}));
const projectId = getProjectId();
const bucketName = admin.storage().bucket().name;
const directoryNames = (apiResponse.prefixes || []).map((prefix:string) => {
//const encodedPrefix = encodeURIComponent(prefix).replace(/'/g, '%27').replace(/%20/g, '+');
const tmpPrefix = prefix.replace(/\/$/, '');
const encodedPrefix = `~2F${tmpPrefix.replace(/\//g, '~2F')}`;
const consoleUrl = `https://console.firebase.google.com/project/${projectId}/storage/${bucketName}/files/${encodedPrefix}`;
return { type: "directory", name: prefix, url: consoleUrl };
});
const result = {
nextPageToken: nextPageToken,
files: [...fileNames, ...directoryNames],
hasMore: nextPageToken !== undefined
};
return {
content:[
{
type: "text",
text: JSON.stringify(result,null,2)
}
]
}
}
/**
* Get file information including metadata and download URL.
* @param {string} filePath - The path of the file to get information for.
* @returns {Promise<{ content: object }>} - A promise that resolves to an object containing file metadata and download URL.
*/
export async function getFileInfo(filePath: string): Promise<{ content: { type: string , text: string }[] }> {
const file = admin.storage().bucket().file(filePath);
const [metadata] = await file.getMetadata();
const [url] = await file.getSignedUrl({
action: 'read',
expires: Date.now() + 1000 * 60 * 60 // 1 hour
});
const result = { metadata, downloadUrl:url };
return {
content: [
{ type:'text', text: JSON.stringify(result,null,2) }
]
};
}
```
--------------------------------------------------------------------------------
/src/lib/firebase/firestoreClient.ts:
--------------------------------------------------------------------------------
```typescript
import { Query, Timestamp } from 'firebase-admin/firestore';
import {db, getProjectId} from './firebaseConfig';
import fs from 'fs';
import path from 'path';
export async function list_collections(documentPath?: string, limit: number = 20, pageToken?: string) {
try {
if (!db) {
return { content: [{ type: 'text', text: 'Firebase is not initialized. SERVICE_ACCOUNT_KEY_PATH environment variable is required.' }], isError: true };
}
let collections;
if (documentPath) {
const docRef = db.doc(documentPath);
collections = await docRef.listCollections();
} else {
collections = await db.listCollections();
}
// Sort collections by name
collections.sort((a, b) => a.id.localeCompare(b.id));
// Find start index
const startIndex = pageToken ? collections.findIndex(c => c.id === pageToken) + 1 : 0;
// Apply limit
const paginatedCollections = collections.slice(startIndex, startIndex + limit);
const projectId = getProjectId();
const collectionData = paginatedCollections.map((collection) => {
const collectionUrl = `https://console.firebase.google.com/project/${projectId}/firestore/data/${documentPath}/${collection.id}`;
return { name: collection.id, url: collectionUrl };
});
return {
content: [{
type: 'text',
text: JSON.stringify({
collections: collectionData,
nextPageToken: collections.length > startIndex + limit ?
paginatedCollections[paginatedCollections.length - 1].id : null,
hasMore: collections.length > startIndex + limit
})
}]
};
} catch (error) {
return { content: [{ type: 'text', text: `Error listing collections: ${(error as Error).message}` }], isError: true };
}
}
function convertTimestampsToISO(data: any) {
for (const key in data) {
if (data[key] instanceof Timestamp) {
data[key] = data[key].toDate().toISOString();
}
}
return data;
}
export async function listDocuments(collection: string, filters: Array<{ field: string, operator: FirebaseFirestore.WhereFilterOp, value: any }> = [], limit: number = 20, pageToken?: string) {
const projectId = getProjectId();
try {
if (!db) {
return { content: [{ type: 'text', text: 'Firebase is not initialized. SERVICE_ACCOUNT_KEY_PATH environment variable is required.' }], isError: true };
}
const collectionRef = db.collection(collection);
let filteredQuery: Query = collectionRef;
for (const filter of filters) {
let filterValue = filter.value;
if (typeof filterValue === 'string' && !isNaN(Date.parse(filterValue))) {
filterValue = Timestamp.fromDate(new Date(filterValue));
}
filteredQuery = filteredQuery.where(filter.field, filter.operator, filterValue);
}
// Apply pageToken if provided
if (pageToken) {
const startAfterDoc = await collectionRef.doc(pageToken).get();
filteredQuery = filteredQuery.startAfter(startAfterDoc);
}
// Get total count of documents matching the filter
const countSnapshot = await filteredQuery.get();
const totalCount = countSnapshot.size;
// Get the first 'limit' documents
const limitedQuery = filteredQuery.limit(limit);
const snapshot = await limitedQuery.get();
if (snapshot.empty) {
return { content: [{ type: 'text', text: 'No matching documents found' }], isError: true };
}
const documents = snapshot.docs.map((doc: any) => {
const data = doc.data();
convertTimestampsToISO(data);
const consoleUrl = `https://console.firebase.google.com/project/${projectId}/firestore/data/${collection}/${doc.id}`;
return { id: doc.id, url: consoleUrl, document: data };
});
return {
content: [{
type: 'text',
text: JSON.stringify({
totalCount,
documents,
pageToken: documents.length > 0 ? documents[documents.length - 1].id : null,
hasMore: totalCount > limit
})
}]
};
} catch (error) {
return { content: [{ type: 'text', text: `Error listing documents: ${(error as Error).message}` }], isError: true };
}
}
export async function addDocument(collection: string, data: any) {
try {
if (!db) {
return { content: [{ type: 'text', text: 'Firebase is not initialized. SERVICE_ACCOUNT_KEY_PATH environment variable is required.' }], isError: true };
}
const docRef = await db.collection(collection).add(data);
const projectId = getProjectId();
convertTimestampsToISO(data);
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, document: data }) }] };
} catch (error) {
return { content: [{ type: 'text', text: `Error adding document: ${(error as Error).message}` }], isError: true };
}
}
export async function getDocument(collection: string, id: string) {
try {
if (!db) {
return { content: [{ type: 'text', text: 'Firebase is not initialized. SERVICE_ACCOUNT_KEY_PATH environment variable is required.' }], isError: true };
}
const doc = await db.collection(collection).doc(id).get();
if (!doc.exists) {
return { content: [{ type: 'text', text: 'Document not found' }], isError: true };
}
const projectId = getProjectId();
const data = doc.data();
convertTimestampsToISO(data);
const consoleUrl = `https://console.firebase.google.com/project/${projectId}/firestore/data/${collection}/${id}`;
return { content: [{ type: 'text', text: JSON.stringify({ id, url: consoleUrl, document: data }) }] };
} catch (error) {
return { content: [{ type: 'text', text: `Error getting document: ${(error as Error).message}` }], isError: true };
}
}
export async function updateDocument(collection: string, id: string, data: any) {
try {
if (!db) {
return { content: [{ type: 'text', text: 'Firebase is not initialized. SERVICE_ACCOUNT_KEY_PATH environment variable is required.' }], isError: true };
}
await db.collection(collection).doc(id).update(data);
const projectId = getProjectId();
convertTimestampsToISO(data);
const consoleUrl = `https://console.firebase.google.com/project/${projectId}/firestore/data/${collection}/${id}`;
return { content: [{ type: 'text', text: JSON.stringify({ id, url: consoleUrl, document: data }) }] };
} catch (error) {
return { content: [{ type: 'text', text: `Error updating document: ${(error as Error).message}` }], isError: true };
}
}
export async function deleteDocument(collection: string, id: string) {
try {
if (!db) {
return { content: [{ type: 'text', text: 'Firebase is not initialized. SERVICE_ACCOUNT_KEY_PATH environment variable is required.' }], isError: true };
}
await db.collection(collection).doc(id).delete();
return { content: [{ type: 'text', text: 'Document deleted successfully' }] };
} catch (error) {
return { content: [{ type: 'text', text: `Error deleting document: ${(error as Error).message}` }], isError: true };
}
}
```
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
```typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
import { addDocument, getDocument, updateDocument, deleteDocument, listDocuments, list_collections } from './lib/firebase/firestoreClient';
import { db } from './lib/firebase/firebaseConfig';
import { listDirectoryFiles, getFileInfo } from './lib/firebase/storageClient';
import { getUserByIdOrEmail } from './lib/firebase/authClient';
class FirebaseMcpServer {
private server: Server;
constructor() {
this.server = new Server(
{
name: 'firebase-mcp-server',
version: '0.9.0'
},
{
capabilities: {
tools: {}
}
}
);
this.setupToolHandlers();
this.server.onerror = (error) => console.error('[MCP Error]', error);
process.on('SIGINT', async () => {
await this.server.close();
process.exit(0);
});
}
private setupToolHandlers() {
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_collections',
description: 'List collections in Firestore. If documentPath is provided, returns subcollections under that document; otherwise returns root collections.',
inputSchema: {
type: 'object',
properties: {
documentPath: {
type: 'string',
description: 'Optional parent document path'
},
limit: {
type: 'number',
description: 'Number of collections to return',
default: 20
},
pageToken: {
type: 'string',
description: 'Token for pagination to get the next page of results'
}
},
required: []
}
},
{
name: 'firestore_list_documents',
description: 'List documents from a Firestore collection with optional filtering',
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'
},
value: {
type: 'any',
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'
}
},
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"]
}
}
]
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args = {} } = request.params;
switch (name) {
case 'firestore_add_document':
return addDocument(args.collection as string, args.data as object);
case 'firestore_list_documents':
return listDocuments(
args.collection as string,
args.filters as Array<{ field: string, operator: FirebaseFirestore.WhereFilterOp, value: any }>,
args.limit as number,
args.pageToken as string | undefined
);
case 'firestore_get_document':
return getDocument(args.collection as string, args.id as string);
case 'firestore_update_document':
return updateDocument(args.collection as string, args.id as string, args.data as object);
case 'firestore_delete_document':
return deleteDocument(args.collection as string, args.id as string);
case 'firestore_list_collections':
return list_collections(
args.documentPath as string | undefined,
args.limit as number | undefined,
args.pageToken as string | undefined
);
case 'auth_get_user':
return getUserByIdOrEmail(args.identifier as string);
case 'storage_list_files':
return listDirectoryFiles(
args.directoryPath as string | undefined,
args.pageSize as number | undefined,
args.pageToken as string | undefined
);
case 'storage_get_file_info':
return getFileInfo(args.filePath as string);
default:
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Firebase MCP server running on stdio');
}
}
const server = new FirebaseMcpServer();
server.run().catch(console.error);
```