Skip to content

Add firestore_count_documents #84

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 81 additions & 1 deletion src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,9 @@ describe('Firebase MCP Server', () => {
vi.resetModules();

// Set up mocks again
vi.doMock('@modelcontextprotocol/sdk/server/index.js', () => ({ Server: serverConstructor }));
vi.doMock('@modelcontextprotocol/sdk/server/index.js', () => ({
Server: serverConstructor,
}));
vi.doMock('../utils/logger', () => ({ logger: loggerMock }));
vi.doMock('firebase-admin', () => adminMock);

Expand Down Expand Up @@ -1833,6 +1835,84 @@ describe('Firebase MCP Server', () => {
expect(content).toHaveProperty('error', 'Firebase initialization failed');
});
});

describe('firestore_count_documents', () => {
it('should count documents in a collection', async () => {
// Mock the count().get() chain
const countValue = 5;
const countMock = vi.fn().mockReturnThis();
const getMock = vi.fn().mockResolvedValue({
data: () => ({ count: countValue }),
});

// Mock the query chain
const queryMock = {
where: vi.fn().mockReturnThis(),
count: countMock,
get: getMock,
};

// Mock collection to return the queryMock
const collectionMock = {
where: queryMock.where,
count: queryMock.count,
get: queryMock.get,
};

adminMock.firestore = () => ({
collection: vi.fn().mockReturnValue(collectionMock),
});

const result = await callToolHandler({
params: {
name: 'firestore_count_documents',
arguments: {
collection: 'test',
filters: [{ field: 'status', operator: '==', value: 'active' }],
},
},
});

const content = JSON.parse(result.content[0].text);
expect(content).toHaveProperty('count', countValue);
});

it('should handle missing filters', async () => {
const countValue = 3;
const countMock = vi.fn().mockReturnThis();
const getMock = vi.fn().mockResolvedValue({
data: () => ({ count: countValue }),
});

const queryMock = {
where: vi.fn().mockReturnThis(),
count: countMock,
get: getMock,
};

const collectionMock = {
where: queryMock.where,
count: queryMock.count,
get: queryMock.get,
};

adminMock.firestore = () => ({
collection: vi.fn().mockReturnValue(collectionMock),
});

const result = await callToolHandler({
params: {
name: 'firestore_count_documents',
arguments: {
collection: 'test',
},
},
});

const content = JSON.parse(result.content[0].text);
expect(content).toHaveProperty('count', countValue);
});
});
});

describe('firestore_list_collections', () => {
Expand Down
89 changes: 89 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,44 @@ class FirebaseMcpServer {
required: ['collectionId'],
},
},
{
name: 'firestore_count_documents',
description:
'Count the number of documents in a Firestore collection, with optional filters',
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'],
},
},
},
required: ['collection'],
},
},
],
}));

Expand Down Expand Up @@ -546,6 +584,57 @@ class FirebaseMcpServer {
logger.debug('firestore_add_document response:', JSON.stringify(response));
return response;
}
case 'firestore_count_documents': {
const collection = args.collection as string;
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);
});
}

// Get the count
const snapshot = await query.count().get();
const count = snapshot.data().count;

// Return the count in the response
const response = {
content: [
{
type: 'text',
text: JSON.stringify({ count }),
},
],
};
logger.debug('firestore_count_documents response:', JSON.stringify(response));
return response;
}

case 'firestore_list_documents': {
const collection = args.collection as string;
Expand Down