Skip to content

feat: named Firestore database support#171

Open
mtio wants to merge 5 commits intogannonh:mainfrom
mtio:feat/multi-db
Open

feat: named Firestore database support#171
mtio wants to merge 5 commits intogannonh:mainfrom
mtio:feat/multi-db

Conversation

@mtio
Copy link
Copy Markdown

@mtio mtio commented Apr 3, 2026

Summary

  • Adds support for targeting named Firestore databases (multi-database projects) via:
    • FIRESTORE_DATABASE_ID environment variable as a server-wide default
    • Per-tool databaseId parameter on all Firestore tools
    • Resolution priority: explicit databaseId arg → env var → (default)
  • Adds a new firestore_list_databases tool that lists all databases in the project via the Firestore Admin REST API
  • Introduces getFirestoreInstance() helper that resolves the database ID and returns the correct Firestore instance while reusing the single Firebase app

Test plan

  • 22 new tests in multi-db.test.ts covering schema registration, resolution priority chain, per-tool forwarding for all 7 Firestore tools, and list databases tool
  • 2 new tests in config.test.ts for the FIRESTORE_DATABASE_ID env var
  • All existing tests continue to pass
  • Format, lint, and build all pass (publish-preflight)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Multi-database Firestore support with optional FIRESTORE_DATABASE_ID and per-call database selection
    • New tool to list/discover all Firestore databases in a project
  • Documentation

    • Added CLAUDE.md guidance and updated README with database selection and new tool details
  • Tests

    • Added test suites covering multi-database behavior, database-listing, and related error handling
  • Chores

    • Added .claude local settings to .gitignore to avoid committing local config

mtio and others added 3 commits April 3, 2026 12:08
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add support for targeting named Firestore databases via:
- FIRESTORE_DATABASE_ID env var as a server-wide default
- Per-tool databaseId parameter on all Firestore tools

Resolution priority: explicit databaseId arg → env var → (default).

Adds getFirestoreInstance() helper that resolves the database ID and
returns the appropriate Firestore instance while reusing the single
Firebase app.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Lists all Firestore databases in the project via the Firestore Admin
REST API using the existing service account credentials. Useful for
discovering available database IDs to pass to the databaseId parameter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 3, 2026

📝 Walkthrough

Walkthrough

Adds multi-database Firestore support: new FIRESTORE_DATABASE_ID config, optional per-call databaseId for Firestore tools, database resolution helper, new firestore_list_databases tool, tests, docs, and a .gitignore entry.

Changes

Cohort / File(s) Summary
Ignore & Docs
/.gitignore, /CLAUDE.md, /README.md
Ignored .claude/settings.local.json; added CLAUDE.md; documented new FIRESTORE_DATABASE_ID, per-call databaseId precedence, and firestore_list_databases tool in README.
Config
src/config.ts
Added `firestoreDatabaseId: string
Server & Tools
src/index.ts
Added getFirestoreInstance(databaseId?) resolving per-call → config → '(default)'; extended Firestore tool schemas to accept optional databaseId; updated handlers to use resolved Firestore instance; added firestore_list_databases tool using REST fetch with timeout and error handling.
Firestore Client
src/lib/firebase/firestoreClient.ts
list_collections() now accepts optional firestoreInstance?: FirebaseFirestore.Firestore and derives client from that or admin.firestore(), removing previous admin-type validation.
Tests — Config
src/__tests__/config.test.ts
Added tests for FIRESTORE_DATABASE_ID handling and firestoreDatabaseId presence/null behavior.
Tests — Multi-DB
src/__tests__/multi-db.test.ts
New suite validating databaseId schema exposure, resolution precedence (per-call → config → default), per-tool forwarding, and firestore_list_databases REST behavior and errors.
Tests — Setup & Index
vitest.setup.ts, src/__tests__/index.test.ts
Test setup no longer rethrows Firebase init errors; added mocks in index tests to exercise alternate failure paths.
Repo metadata
multiple files
Mostly documentation and tests; public API additions: ServerConfig.firestoreDatabaseId, exported getFirestoreInstance, and new MCP tool firestore_list_databases.

Sequence Diagram

sequenceDiagram
    participant Client as "Client"
    participant MCP as "MCP Server"
    participant Handler as "Tool Handler"
    participant Resolver as "getFirestoreInstance"
    participant Admin as "Firebase Admin SDK"
    participant DB as "Firestore DB"

    Client->>MCP: Invoke Firestore tool (optional databaseId)
    MCP->>Handler: Route request
    Handler->>Resolver: resolve(databaseId?)
    Resolver->>Resolver: choose param -> config.firestoreDatabaseId -> "(default)"
    Resolver->>Admin: getFirestore(resolvedId)
    Admin->>DB: return Firestore instance
    Resolver-->>Handler: Firestore instance
    Handler->>DB: perform operation (doc/collection/query)
    DB-->>Handler: operation result
    Handler-->>MCP: formatted tool response
    MCP-->>Client: return result
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hop through code and sniff each trail,

per-call then config, never fail.
I list the databases, big and small,
resolve each home, I fetch them all.
🥕 Hooray — every Firestore now hears my call.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: named Firestore database support' directly and clearly summarizes the main change: adding multi-database support via named Firestore databases, which is the primary objective throughout the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.gitignore:
- Around line 72-75: The new .claude ignore entry has an extra blank line before
it; remove one of the blank lines so the `.claude/settings.local.json` entry is
separated from the previous section by a single blank line (matching the
existing section spacing used elsewhere), ensuring consistency in the .gitignore
formatting.

In `@CLAUDE.md`:
- Around line 27-33: The README line in CLAUDE.md incorrectly states "Registers
12 MCP tools"; update the documentation to reflect the current code in
src/index.ts where FirebaseMcpServer (and its setupToolHandlers) now registers
13 tools (including the newly added firestore_list_databases). Edit the bullet
under "Server Core (`src/index.ts`)" to say "Registers 13 MCP tools" so the doc
matches the FirebaseMcpServer/setupToolHandlers implementation.

In `@src/__tests__/multi-db.test.ts`:
- Around line 109-110: The fake app in setupModule does not provide the fields
the new firestore_list_databases handler expects (app.options.projectId and
app.options.credential.getAccessToken), so update the fakeApp created in
setupModule to include an options object with a projectId string and a
credential object that implements getAccessToken (returning a Promise or object
with access_token and expiry_date as the real credential does). Locate the
fakeApp inside the setupModule helper in src/__tests__/multi-db.test.ts and add
options: { projectId: 'test-project', credential: { getAccessToken: async () =>
({ access_token: 'fake-token', expiry_date: Date.now() + 3600 * 1000 }) } } so
the happy-path tests exercise the new code path.

In `@src/index.ts`:
- Around line 1601-1604: The Firestore Admin API fetch (building `url` and
calling `fetch` with `Authorization: Bearer ${tokenResult.access_token}`) has no
timeout/abort handling; wrap this call with an AbortController and a setTimeout
that calls controller.abort() after a reasonable ms (e.g. 5s), pass
controller.signal into fetch, and clear the timeout on success; update the
surrounding tool handler to catch the abort error (or a DOMException with name
"AbortError") and return/throw a bounded timeout error instead of letting the
request hang.
- Around line 1585-1589: The projectId resolution in firestore_list_databases is
using credential.getAccessToken() and app.options.projectId which can be empty
for service-account initialization; update it to reuse the same service-account
parsing helper used in initializeFirebase (or call the existing getProjectId
helper) so the resolved projectId comes from the same source used during
initialization (e.g., replace the current projectId lookup with
getProjectId(app) or the service-account parser used in initializeFirebase), and
add the corresponding getProjectId import to the top of the file so
firestore_list_databases and any related code use a consistent project ID.

In `@vitest.setup.ts`:
- Around line 64-67: The current catch in the Firebase initialization (in
vitest.setup.ts) swallows all errors; change this so failures fail fast unless
an explicit opt-in for mocked suites is present: detect an env flag like
USE_FIREBASE_MOCKS (or a provided config) and only suppress the error when that
flag is true; otherwise rethrow the caught error (or call process.exit(1)) so
the test run fails immediately and downstream beforeAll/afterAll auth failures
are not hidden.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c7d11c0f-8dae-448a-9282-e30ba8c656e6

📥 Commits

Reviewing files that changed from the base of the PR and between 4327231 and c02995f.

📒 Files selected for processing (9)
  • .gitignore
  • CLAUDE.md
  • README.md
  • src/__tests__/config.test.ts
  • src/__tests__/multi-db.test.ts
  • src/config.ts
  • src/index.ts
  • src/lib/firebase/firestoreClient.ts
  • vitest.setup.ts

Comment thread .gitignore
Comment thread CLAUDE.md
Comment on lines +27 to +33
### Server Core (`src/index.ts`)
Single `FirebaseMcpServer` class that:
1. Initializes Firebase Admin SDK from `SERVICE_ACCOUNT_KEY_PATH`
2. Registers 12 MCP tools via `setupToolHandlers()` (ListTools + CallTool handlers)
3. Dispatches tool calls through a switch statement
4. Connects to a transport (stdio or HTTP)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update the documented tool count.

src/index.ts now registers 13 tools after adding firestore_list_databases, so "Registers 12 MCP tools" is already stale.

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 27-27: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` around lines 27 - 33, The README line in CLAUDE.md incorrectly
states "Registers 12 MCP tools"; update the documentation to reflect the current
code in src/index.ts where FirebaseMcpServer (and its setupToolHandlers) now
registers 13 tools (including the newly added firestore_list_databases). Edit
the bullet under "Server Core (`src/index.ts`)" to say "Registers 13 MCP tools"
so the doc matches the FirebaseMcpServer/setupToolHandlers implementation.

Comment thread src/__tests__/multi-db.test.ts Outdated
Comment thread src/index.ts
Comment thread src/index.ts Outdated
Comment on lines +1601 to +1604
const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases`;
const response = await fetch(url, {
headers: { Authorization: `Bearer ${tokenResult.access_token}` },
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add a timeout to the Firestore Admin API call.

This fetch runs directly inside the tool handler with no abort signal. A slow or blackholed network request will hang the MCP call indefinitely instead of returning a bounded error.

Suggested timeout wrapper
               const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases`;
-              const response = await fetch(url, {
-                headers: { Authorization: `Bearer ${tokenResult.access_token}` },
-              });
+              const controller = new AbortController();
+              const timeoutId = setTimeout(() => controller.abort(), 5000);
+
+              let response: Response;
+              try {
+                response = await fetch(url, {
+                  headers: { Authorization: `Bearer ${tokenResult.access_token}` },
+                  signal: controller.signal,
+                });
+              } finally {
+                clearTimeout(timeoutId);
+              }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases`;
const response = await fetch(url, {
headers: { Authorization: `Bearer ${tokenResult.access_token}` },
});
const url = `https://firestore.googleapis.com/v1/projects/${projectId}/databases`;
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
let response: Response;
try {
response = await fetch(url, {
headers: { Authorization: `Bearer ${tokenResult.access_token}` },
signal: controller.signal,
});
} finally {
clearTimeout(timeoutId);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 1601 - 1604, The Firestore Admin API fetch
(building `url` and calling `fetch` with `Authorization: Bearer
${tokenResult.access_token}`) has no timeout/abort handling; wrap this call with
an AbortController and a setTimeout that calls controller.abort() after a
reasonable ms (e.g. 5s), pass controller.signal into fetch, and clear the
timeout on success; update the surrounding tool handler to catch the abort error
(or a DOMException with name "AbortError") and return/throw a bounded timeout
error instead of letting the request hang.

Comment thread vitest.setup.ts
Comment on lines 64 to 67
} catch (error) {
console.error('Firebase initialization failed:', error);
throw error;
// Don't throw — unit tests mock Firebase and don't need real credentials
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't suppress Firebase bootstrap failures for the whole test run.

This file runs before every suite. Swallowing the initialization error here means a broken emulator/service-account setup only gets logged, while the later beforeAll/afterAll auth failures are swallowed too, so integration tests can continue in an invalid environment. Gate this behavior behind an explicit opt-in for mocked suites, or keep failing fast here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vitest.setup.ts` around lines 64 - 67, The current catch in the Firebase
initialization (in vitest.setup.ts) swallows all errors; change this so failures
fail fast unless an explicit opt-in for mocked suites is present: detect an env
flag like USE_FIREBASE_MOCKS (or a provided config) and only suppress the error
when that flag is true; otherwise rethrow the caught error (or call
process.exit(1)) so the test run fails immediately and downstream
beforeAll/afterAll auth failures are not hidden.

- Fix stale tool count in CLAUDE.md
- Remove extra blank line in .gitignore
- Add projectId and credential to fakeApp mock so list_databases
  happy path is properly asserted
- Resolve projectId via getProjectId() fallback instead of casting
  credential object
- Add 10s timeout to Firestore Admin API fetch call
- Fix broken error-handling test by adding missing module mocks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CLAUDE.md`:
- Around line 27-44: Add a blank line immediately after each "###" subsection
heading in CLAUDE.md to satisfy markdownlint MD022; specifically update the
headings "Server Core (`src/index.ts`)", "Firebase Service Clients
(`src/lib/firebase/`)", "Transport Layer (`src/transports/`)", and
"Configuration (`src/config.ts`)" so each is followed by one empty line before
the paragraph or list that follows.

In `@src/__tests__/index.test.ts`:
- Around line 2597-2605: The test currently mocks getFirestore to throw (via
vi.doMock('firebase-admin/firestore')) but still allows the generic fallback
assertion "Firebase initialization failed", so tighten the assertion to require
the mocked failure: update the test's expect/should-reject to assert that the
thrown error originates from the mocked getFirestore (e.g., error.message
includes genericError.message or equals genericError.message) rather than the
generic fallback string; ensure the test references the
vi.doMock('firebase-admin/firestore') mock and the genericError symbol so the
test only passes when getFirestore actually throws.

In `@src/__tests__/multi-db.test.ts`:
- Around line 100-103: The afterEach currently calls vi.resetModules() and
vi.clearAllMocks() but does not restore any overwritten global.fetch, which lets
stubs leak between tests; capture the original fetch (e.g. const ORIGINAL_FETCH
= global.fetch at module scope or save it before each stub) and restore it in
afterEach by assigning global.fetch = ORIGINAL_FETCH (or using a saved
reference), ensuring this restore runs alongside
vi.resetModules/vi.clearAllMocks so that tests which stub global.fetch are
cleaned up; update the tests that stub fetch (around the blocks referenced and
any other places) to either save/restore their original fetch or rely on the
shared ORIGINAL_FETCH restoration in afterEach.

In `@src/index.ts`:
- Around line 1586-1610: The firestore_list_databases path currently always
fetches a real OAuth token and calls the live Firestore Admin API (see
tokenResult, projectId, url, fetch); update it to first check for emulator mode
by looking for process.env.FIRESTORE_EMULATOR_HOST (or equivalent config flag)
and if present short-circuit: do not call credential.getAccessToken() or fetch
the Admin API, instead return the appropriate emulator-local response (or query
the emulator host) so the code stays local to the emulator; ensure tokenResult,
fetch call and AbortController logic are skipped when FIRESTORE_EMULATOR_HOST is
set.
- Around line 90-95: getFirestoreInstance currently treats empty or
whitespace-only databaseId as "not provided" and falls back to
config.firestoreDatabaseId; change it to first normalize and validate the
explicit input: if databaseId is provided (not null/undefined) but is an empty
or whitespace-only string (e.g., databaseId.trim() === ''), throw a clear Error
indicating an invalid databaseId; otherwise use the explicit databaseId, then
fall back to config.firestoreDatabaseId using the nullish coalescing operator
(config.firestoreDatabaseId ?? '(default)') before calling getFirestore(app,
resolvedId). Ensure validation logic sits at the start of getFirestoreInstance
and references the databaseId parameter and config.firestoreDatabaseId.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2a56d982-ac67-4842-b010-e1c6877dae54

📥 Commits

Reviewing files that changed from the base of the PR and between c02995f and 9505bae.

📒 Files selected for processing (5)
  • .gitignore
  • CLAUDE.md
  • src/__tests__/index.test.ts
  • src/__tests__/multi-db.test.ts
  • src/index.ts

Comment thread CLAUDE.md
Comment on lines +27 to +44
### Server Core (`src/index.ts`)
Single `FirebaseMcpServer` class that:
1. Initializes Firebase Admin SDK from `SERVICE_ACCOUNT_KEY_PATH`
2. Registers MCP tools via `setupToolHandlers()` (ListTools + CallTool handlers)
3. Dispatches tool calls through a switch statement
4. Connects to a transport (stdio or HTTP)

### Firebase Service Clients (`src/lib/firebase/`)
- `firestoreClient.ts` - Firestore CRUD, queries, collection group queries
- `storageClient.ts` - File list/info/upload (local, base64, text, URL)
- `authClient.ts` - User lookup by email or UID

### Transport Layer (`src/transports/`)
- `index.ts` - Factory that selects stdio vs HTTP based on config
- `http.ts` - Express.js server with session-based SSE for MCP over HTTP

### Configuration (`src/config.ts`)
Environment variables loaded via dotenv. `SERVICE_ACCOUNT_KEY_PATH` is required; transport defaults to stdio. `FIRESTORE_DATABASE_ID` optionally targets a named Firestore database. Auto-detects stdio context from TTY state.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add blank lines after these subsection headings.

markdownlint is already flagging MD022 here, so the docs/lint step will keep failing until each ### heading is followed by a blank line.

🧰 Tools
🪛 markdownlint-cli2 (0.22.0)

[warning] 27-27: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 34-34: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 39-39: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 43-43: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CLAUDE.md` around lines 27 - 44, Add a blank line immediately after each
"###" subsection heading in CLAUDE.md to satisfy markdownlint MD022;
specifically update the headings "Server Core (`src/index.ts`)", "Firebase
Service Clients (`src/lib/firebase/`)", "Transport Layer (`src/transports/`)",
and "Configuration (`src/config.ts`)" so each is followed by one empty line
before the paragraph or list that follows.

Comment on lines +2597 to +2605
vi.doMock('firebase-admin/firestore', () => ({
Timestamp: { fromDate: vi.fn() },
getFirestore: vi.fn().mockImplementation(() => {
throw genericError;
}),
}));
vi.doMock('../lib/firebase/firebaseConfig.js', () => ({
getProjectId: vi.fn().mockReturnValue('test-project'),
}));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make this test require the mocked getFirestore() failure.

With Lines 2597-2605 now forcing getFirestore() to throw, the fallback branch below that still accepts "Firebase initialization failed" keeps this test green on the same regression it was meant to catch.

Tighten the assertion in this test
-      if (content.error === 'Firebase initialization failed') {
-        expect(content).toHaveProperty('error');
-      } else {
-        expect(result.content[0].text).toContain('Generic Firestore error');
-      }
+      expect(result.content[0].text).toContain('Generic Firestore error');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/index.test.ts` around lines 2597 - 2605, The test currently
mocks getFirestore to throw (via vi.doMock('firebase-admin/firestore')) but
still allows the generic fallback assertion "Firebase initialization failed", so
tighten the assertion to require the mocked failure: update the test's
expect/should-reject to assert that the thrown error originates from the mocked
getFirestore (e.g., error.message includes genericError.message or equals
genericError.message) rather than the generic fallback string; ensure the test
references the vi.doMock('firebase-admin/firestore') mock and the genericError
symbol so the test only passes when getFirestore actually throws.

Comment on lines +100 to +103
afterEach(() => {
vi.resetModules();
vi.clearAllMocks();
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Restore fetch after stubbing it in these cases.

These tests overwrite global.fetch, but the current afterEach never puts the original global back. That can leak this stub into later cases in the same worker.

Simple cleanup pattern
 let loggerMock: Record<string, ReturnType<typeof vi.fn>>;
 let getFirestoreMock: ReturnType<typeof vi.fn>;
 let firestoreInstanceMock: ReturnType<typeof createFirestoreMock>;
+let originalFetch: typeof global.fetch;

 describe('Multi-database support', () => {
   beforeEach(async () => {
     vi.resetModules();
     vi.clearAllMocks();
+    originalFetch = global.fetch;
@@
   afterEach(() => {
+    global.fetch = originalFetch;
     vi.resetModules();
     vi.clearAllMocks();
   });
@@
-      global.fetch = mockFetch;
+      global.fetch = mockFetch as typeof global.fetch;
@@
-      global.fetch = mockFetch;
+      global.fetch = mockFetch as typeof global.fetch;

Also applies to: 372-390, 412-417

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/__tests__/multi-db.test.ts` around lines 100 - 103, The afterEach
currently calls vi.resetModules() and vi.clearAllMocks() but does not restore
any overwritten global.fetch, which lets stubs leak between tests; capture the
original fetch (e.g. const ORIGINAL_FETCH = global.fetch at module scope or save
it before each stub) and restore it in afterEach by assigning global.fetch =
ORIGINAL_FETCH (or using a saved reference), ensuring this restore runs
alongside vi.resetModules/vi.clearAllMocks so that tests which stub global.fetch
are cleaned up; update the tests that stub fetch (around the blocks referenced
and any other places) to either save/restore their original fetch or rely on the
shared ORIGINAL_FETCH restoration in afterEach.

Comment thread src/index.ts
Comment on lines +90 to +95
function getFirestoreInstance(databaseId?: string): FirebaseFirestore.Firestore {
const resolvedId = databaseId || config.firestoreDatabaseId || '(default)';
if (!app) {
throw new Error('Firebase app is not initialized');
}
return getFirestore(app, resolvedId);
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the structure and find the src/index.ts file
git ls-files | head -20

Repository: gannonh/firebase-mcp

Length of output: 462


🏁 Script executed:

# Let's examine the exact lines in question
if [ -f "src/index.ts" ]; then
  wc -l src/index.ts
fi

Repository: gannonh/firebase-mcp

Length of output: 81


🏁 Script executed:

# Read lines 85-100 to see the context
sed -n '85,100p' src/index.ts

Repository: gannonh/firebase-mcp

Length of output: 642


🏁 Script executed:

# Also search for the firestore_list_databases function mentioned
rg "firestore_list_databases" -A 5

Repository: gannonh/firebase-mcp

Length of output: 3269


🏁 Script executed:

# Find all calls to getFirestoreInstance
rg "getFirestoreInstance" -B 2 -A 2

Repository: gannonh/firebase-mcp

Length of output: 4110


🏁 Script executed:

# Search for where databaseId comes from in the handlers
rg "databaseId" src/index.ts | head -30

Repository: gannonh/firebase-mcp

Length of output: 1308


🏁 Script executed:

# Look at the firestore_list_databases implementation in detail
sed -n '1558,1667p' src/index.ts

Repository: gannonh/firebase-mcp

Length of output: 3803


🏁 Script executed:

# Check the test file to see how databaseId is tested
sed -n '1,50p' src/__tests__/multi-db.test.ts | grep -A 30 "getFirestoreInstance resolution"

Repository: gannonh/firebase-mcp

Length of output: 46


🏁 Script executed:

# Look for test cases involving blank or empty databaseId
rg "databaseId.*['\"']" src/__tests__/multi-db.test.ts

Repository: gannonh/firebase-mcp

Length of output: 1088


🏁 Script executed:

# Check if there are any handlers that might pass empty strings
rg "args.databaseId" -B 5 src/index.ts | head -50

Repository: gannonh/firebase-mcp

Length of output: 2081


🏁 Script executed:

# Look for the structure of databaseId schema definition
rg "databaseId" src/index.ts -B 3 -A 1 | grep -A 5 "databaseIdProperty"

Repository: gannonh/firebase-mcp

Length of output: 1459


🏁 Script executed:

# Get the complete databaseIdProperty definition
rg "const databaseIdProperty" -A 5 src/index.ts

Repository: gannonh/firebase-mcp

Length of output: 275


🏁 Script executed:

# Check if there's any input validation before args is used
rg "args\s*=.*as|args\s*=.*\?" src/index.ts | head -20

Repository: gannonh/firebase-mcp

Length of output: 46


🏁 Script executed:

# Look for the CallToolRequest handling and argument validation
rg "CallToolRequest|call_tool" -A 10 src/index.ts | head -50

Repository: gannonh/firebase-mcp

Length of output: 851


🏁 Script executed:

# Check how the tool arguments are processed and if there's validation
sed -n '200,250p' src/index.ts

Repository: gannonh/firebase-mcp

Length of output: 1784


🏁 Script executed:

# Look for where the tools are registered to understand the flow
sed -n '100,150p' src/index.ts

Repository: gannonh/firebase-mcp

Length of output: 1441


🏁 Script executed:

# Search for emulator-related code
rg "emulator|FIRESTORE_EMULATOR" src/

Repository: gannonh/firebase-mcp

Length of output: 4924


🏁 Script executed:

# Check how the config is structured
cat src/config.ts

Repository: gannonh/firebase-mcp

Length of output: 4256


🏁 Script executed:

# Look at tests to see if blank databaseId is tested
rg "databaseId.*['\"']" src/__tests__/

Repository: gannonh/firebase-mcp

Length of output: 1491


🏁 Script executed:

# Examine the firestore_list_databases handler more closely to understand the REST API call
sed -n '1558,1620p' src/index.ts

Repository: gannonh/firebase-mcp

Length of output: 2250


🏁 Script executed:

# Check if there's any client-side validation of databaseId before it reaches the handler
rg "databaseId" src/index.ts -B 5 | grep -A 5 "validate\|trim\|check\|error"

Repository: gannonh/firebase-mcp

Length of output: 46


🏁 Script executed:

# Look at how the MCP framework handles arguments - is there schema validation?
rg "CallToolRequest" -A 20 src/index.ts | head -40

Repository: gannonh/firebase-mcp

Length of output: 1251


🏁 Script executed:

# Check if there are any tests that attempt to pass empty strings as databaseId
rg "databaseId.*['\"][ ]*['\"]" src/__tests__/

Repository: gannonh/firebase-mcp

Length of output: 46


🏁 Script executed:

# Verify the actual runtime behavior - look at the full handler to see if args validation happens
sed -n '560,600p' src/index.ts

Repository: gannonh/firebase-mcp

Length of output: 1485


🏁 Script executed:

# Check if there's any schema-level validation or if MCP framework validates against the schema
rg "properties\|required" src/index.ts | grep -A 2 "databaseId"

Repository: gannonh/firebase-mcp

Length of output: 46


🏁 Script executed:

# Verify the exact flow - when arguments come in, are they validated against schema?
sed -n '240,280p' src/index.ts

Repository: gannonh/firebase-mcp

Length of output: 1321


🏁 Script executed:

# Check if firebase-admin's getFirestore function handles empty strings gracefully
rg "getFirestore" src/ -A 2 | head -30

Repository: gannonh/firebase-mcp

Length of output: 1541


🏁 Script executed:

# Look for any trim or validation logic on databaseId anywhere in the codebase
rg "trim|isEmpty|isBlank" src/index.ts

Repository: gannonh/firebase-mcp

Length of output: 46


🏁 Script executed:

# Double-check the falsy behavior - verify empty string behavior in JavaScript context
python3 << 'EOF'
# Simulate JavaScript || behavior for empty string
empty_string = ""
config_value = "configured-db"
default_value = "(default)"

# This is how JavaScript || works with empty strings
result = empty_string or config_value or default_value
print(f"Empty string: '{empty_string}'")
print(f"Result of empty || configured || default: '{result}'")
print(f"Empty string is falsy: {not bool(empty_string)}")
EOF

Repository: gannonh/firebase-mcp

Length of output: 166


🏁 Script executed:

# Check if there are any real-world examples or tests that pass empty strings
rg "databaseId" src/__tests__/ -B 3 -A 3 | grep -E "databaseId.*['\"]|test.*empty|blank"

Repository: gannonh/firebase-mcp

Length of output: 1491


🏁 Script executed:

# Search for any error handling or defensive checks elsewhere in the codebase
rg "databaseId" src/index.ts | wc -l

Repository: gannonh/firebase-mcp

Length of output: 66


🏁 Script executed:

# Let's verify what firebase-admin's getFirestore actually does with empty strings
# Check documentation or behavior by looking at how it's used
rg "getFirestore" -B 3 -A 3 src/ | grep -v test

Repository: gannonh/firebase-mcp

Length of output: 4424


🏁 Script executed:

# Check if there are any guards or try-catch blocks around getFirestore calls
sed -n '640,680p' src/index.ts

Repository: gannonh/firebase-mcp

Length of output: 1744


🏁 Script executed:

# Verify: can the MCP schema validation prevent empty strings from being passed?
rg "minLength|pattern|enum" src/index.ts | head -20

Repository: gannonh/firebase-mcp

Length of output: 153


Reject blank databaseId values instead of silently falling through.

On line 91, databaseId || config.firestoreDatabaseId || '(default)' treats empty strings as "not provided". If a client explicitly passes an empty databaseId and a server-wide FIRESTORE_DATABASE_ID is configured, the application silently operates on the configured database instead of rejecting the invalid input. This can lead to unintended data writes to the wrong database.

Normalize explicit input first, fail on blank values, then apply the fallback chain. The suggested approach using trim() to detect whitespace-only strings, explicit validation, and the nullish coalescing operator (??) prevents this silent failure:

Normalization approach
 function getFirestoreInstance(databaseId?: string): FirebaseFirestore.Firestore {
-  const resolvedId = databaseId || config.firestoreDatabaseId || '(default)';
+  const explicitDatabaseId = databaseId?.trim();
+  const configuredDatabaseId = config.firestoreDatabaseId?.trim() || undefined;
+
+  if (databaseId !== undefined && !explicitDatabaseId) {
+    throw new Error('databaseId must not be empty');
+  }
+
+  const resolvedId = explicitDatabaseId ?? configuredDatabaseId ?? '(default)';
   if (!app) {
     throw new Error('Firebase app is not initialized');
   }
   return getFirestore(app, resolvedId);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 90 - 95, getFirestoreInstance currently treats
empty or whitespace-only databaseId as "not provided" and falls back to
config.firestoreDatabaseId; change it to first normalize and validate the
explicit input: if databaseId is provided (not null/undefined) but is an empty
or whitespace-only string (e.g., databaseId.trim() === ''), throw a clear Error
indicating an invalid databaseId; otherwise use the explicit databaseId, then
fall back to config.firestoreDatabaseId using the nullish coalescing operator
(config.firestoreDatabaseId ?? '(default)') before calling getFirestore(app,
resolvedId). Ensure validation logic sits at the start of getFirestoreInstance
and references the databaseId parameter and config.firestoreDatabaseId.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentionally leaving this as-is. The MCP schema defines databaseId as type: "string" — AI clients won't send empty strings here, and if they somehow do, getFirestore(app, "") will throw from the Firebase SDK itself, which is already caught and returned as an error response. Adding trim/validation/nullish-coalescing for this scenario adds complexity for a case that can't happen in practice.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mtio, understood — the combination of the MCP schema type constraint, the Firebase SDK's own validation, and the existing catch-all error handler already covers this path. Happy to leave it as-is.

(˵ •̀ ᴗ - ˵ ) ✧


✏️ Learnings added
Learnt from: mtio
Repo: gannonh/firebase-mcp PR: 171
File: src/index.ts:90-95
Timestamp: 2026-04-04T15:43:30.375Z
Learning: In `src/index.ts` of the `firebase-mcp` project, the `getFirestoreInstance(databaseId?: string)` function intentionally uses `||` (not `??`) for the fallback chain `databaseId || config.firestoreDatabaseId || '(default)'`. Empty-string `databaseId` values are considered invalid input that the MCP schema (`type: "string"`) prevents in practice; if they do slip through, `getFirestore(app, "")` throws from the Firebase SDK itself, which is already caught by the surrounding try/catch handlers. Adding explicit trim/validation/nullish-coalescing was deliberately rejected as unnecessary complexity.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment thread src/index.ts
Return an explicit error when FIRESTORE_EMULATOR_HOST is set, since
the tool calls the live Firestore Admin REST API which would bypass
the emulator.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/index.ts`:
- Around line 641-647: When applying pagination in the
firestore_query_collection and firestore_query_collection_group handlers, do not
silently ignore stale/mismatched page tokens; after resolving pageToken
(args.pageToken) and fetching lastDoc via firestore.doc(pageToken).get(), if
(!lastDoc.exists) throw or return an explicit invalid-token error instead of
falling through to page 1. Update both places that currently only set query =
query.startAfter(lastDoc) to validate lastDoc.exists and return a clear error
(e.g., InvalidPageToken or similar) so callers see an invalid-token response
rather than duplicated first-page results.
- Around line 426-430: The schema for the firestore_list_collections operation
currently only exposes databaseId (inputSchema) but the implementation still
reads args.documentPath and forwards it (firestore_list_collections); update the
inputSchema to include a documentPath property (matching how args.documentPath
is consumed) so discovery-driven MCP clients can see that argument, or remove
the hidden branch that relies on args.documentPath and stop forwarding it in the
firestore_list_collections call — ensure the symbol names to change are
inputSchema and any places referencing
args.documentPath/firestore_list_collections so they remain consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5d48f067-f689-44d8-87b2-9f3e305e068b

📥 Commits

Reviewing files that changed from the base of the PR and between 9505bae and 0e250c1.

📒 Files selected for processing (1)
  • src/index.ts

Comment thread src/index.ts
Comment on lines 426 to +430
inputSchema: {
type: 'object',
properties: {},
properties: {
databaseId: databaseIdProperty,
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Publish documentPath in the schema or remove the hidden branch.

Line 1269 still reads args.documentPath and Line 1291 forwards it, but the advertised firestore_list_collections schema now exposes only databaseId. Discovery-driven MCP clients cannot see that argument, so subcollection requests can silently degrade into root-collection listings when documentPath is dropped.

Suggested schema fix
         {
           name: 'firestore_list_collections',
           description: 'List root collections in Firestore',
           inputSchema: {
             type: 'object',
             properties: {
+              documentPath: {
+                type: 'string',
+                description:
+                  'Optional document path to list subcollections under. Omit to list root collections.',
+              },
               databaseId: databaseIdProperty,
             },
             required: [],
           },
         },

Also applies to: 1268-1295

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 426 - 430, The schema for the
firestore_list_collections operation currently only exposes databaseId
(inputSchema) but the implementation still reads args.documentPath and forwards
it (firestore_list_collections); update the inputSchema to include a
documentPath property (matching how args.documentPath is consumed) so
discovery-driven MCP clients can see that argument, or remove the hidden branch
that relies on args.documentPath and stop forwarding it in the
firestore_list_collections call — ensure the symbol names to change are
inputSchema and any places referencing
args.documentPath/firestore_list_collections so they remain consistent.

Comment thread src/index.ts
Comment on lines 641 to 647
// Apply pagination if pageToken is provided
const pageToken = args.pageToken as string | undefined;
if (pageToken) {
const lastDoc = await admin.firestore().doc(pageToken).get();
const lastDoc = await firestore.doc(pageToken).get();
if (lastDoc.exists) {
query = query.startAfter(lastDoc);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject missing page tokens instead of silently restarting pagination.

Line 644 and Line 1401 only paginate when the token document exists. A stale token — or one copied from another databaseId — currently falls through and returns page 1 again, which makes pagination look successful while duplicating results. Return an explicit invalid-token error when !lastDoc.exists in both handlers.

Suggested guard
             if (pageToken) {
               const lastDoc = await firestore.doc(pageToken).get();
-              if (lastDoc.exists) {
-                query = query.startAfter(lastDoc);
-              }
+              if (!lastDoc.exists) {
+                return {
+                  content: [
+                    {
+                      type: 'text',
+                      text: JSON.stringify({ error: 'Invalid pagination token' }),
+                    },
+                  ],
+                };
+              }
+              query = query.startAfter(lastDoc);
             }

Apply the same !lastDoc.exists check in firestore_query_collection_group.

Also applies to: 1398-1404

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/index.ts` around lines 641 - 647, When applying pagination in the
firestore_query_collection and firestore_query_collection_group handlers, do not
silently ignore stale/mismatched page tokens; after resolving pageToken
(args.pageToken) and fetching lastDoc via firestore.doc(pageToken).get(), if
(!lastDoc.exists) throw or return an explicit invalid-token error instead of
falling through to page 1. Update both places that currently only set query =
query.startAfter(lastDoc) to validate lastDoc.exists and return a clear error
(e.g., InvalidPageToken or similar) so callers see an invalid-token response
rather than duplicated first-page results.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant