-
Notifications
You must be signed in to change notification settings - Fork 1k
Firestore mcp emulator #8664
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
base: master
Are you sure you want to change the base?
Firestore mcp emulator #8664
Changes from all commits
755d7ac
ca0ece6
2fe64c2
cc0afce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { z } from "zod"; | ||
import { tool } from "../../tool.js"; | ||
import { mcpError, toContent } from "../../util.js"; | ||
import { commitDocument, FirestoreDocument, FirestoreValue } from "../../../gcp/firestore.js"; | ||
import { convertInputToValue, firestoreDocumentToJson } from "./converter.js"; | ||
Check failure on line 5 in src/mcp/tools/firestore/commit_document.ts
|
||
import { getFirestoreEmulatorHost } from "./emulator.js"; | ||
import { NO_PROJECT_ERROR } from "../../errors.js"; | ||
|
||
export const commit_document = tool( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's separate it out to a separate PR |
||
{ | ||
name: "commit_document", | ||
description: | ||
"Creates or updates a Firestore document in a database in the current project. If the document does not exist, it will be created. If the document does exist, its contents will be overwritten with the new data.", | ||
inputSchema: z.object({ | ||
// TODO: Support configurable database | ||
// database: z | ||
// .string() | ||
// .nullish() | ||
// .describe("Database id to use. Defaults to `(default)` if unspecified."), | ||
path: z | ||
.string() | ||
.describe( | ||
"A document path (e.g. `collectionName/documentId` or `parentCollection/parentDocument/collectionName/documentId`)", | ||
), | ||
document_data: z | ||
.record(z.any()) | ||
.describe( | ||
"A JSON object representing the fields of the document. For special data types like GeoPoint or Timestamp, refer to Firestore documentation for the correct JSON representation or use simpler types like strings/numbers/booleans/arrays/maps.", | ||
), | ||
use_emulator: z.boolean().default(false).describe("Target the Firestore emulator if true."), | ||
}), | ||
annotations: { | ||
title: "Put Firestore document", | ||
}, | ||
_meta: { | ||
requiresAuth: true, | ||
requiresProject: true, | ||
}, | ||
}, | ||
async ({ path, document_data, use_emulator }, { projectId, host }) => { | ||
if (!projectId) return NO_PROJECT_ERROR; | ||
if (use_emulator) { | ||
const emulatorHost = await getFirestoreEmulatorHost(await host.getEmulatorHubClient()); | ||
if (emulatorHost) { | ||
process.env.FIRESTORE_EMULATOR_HOST = emulatorHost; | ||
} | ||
} | ||
|
||
if (!path || Object.keys(document_data).length === 0) { | ||
return mcpError("Document path and document_data cannot be empty."); | ||
} | ||
|
||
const fields: { [key: string]: FirestoreValue } = {}; | ||
for (const key in document_data) { | ||
if (Object.prototype.hasOwnProperty.call(document_data, key)) { | ||
fields[key] = convertInputToValue(document_data[key]); | ||
} | ||
} | ||
|
||
const firestoreDocToPut: Partial<FirestoreDocument> & { fields: { [key: string]: FirestoreValue } } = { | ||
Check failure on line 60 in src/mcp/tools/firestore/commit_document.ts
|
||
fields, | ||
}; | ||
|
||
try { | ||
const result = await commitDocument(projectId, path, firestoreDocToPut as any, use_emulator); | ||
return toContent(result); | ||
} catch (err: any) { | ||
return mcpError(`Failed to put document at path '${path}': ${err.message || err}`); | ||
} | ||
}, | ||
); | ||
Check failure on line 71 in src/mcp/tools/firestore/commit_document.ts
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { EmulatorHubClient } from "../../../emulator/hubClient.js"; | ||
import { Emulators } from "../../../emulator/types.js"; | ||
|
||
/** | ||
* Gets the Firestore emulator host and port from the Emulator Hub. | ||
* Throws an error if the Emulator Hub or Firestore emulator is not running. | ||
* @param hubClient The EmulatorHubClient instance. | ||
* @returns A string in the format "host:port". | ||
*/ | ||
export async function getFirestoreEmulatorHost(hubClient?: EmulatorHubClient): Promise<string> { | ||
if (!hubClient) { | ||
throw Error( | ||
Check failure on line 12 in src/mcp/tools/firestore/emulator.ts
|
||
"Emulator Hub not found or is not running. Cannot target Firestore emulator.", | ||
); | ||
} | ||
|
||
const emulators = await hubClient.getEmulators(); | ||
const firestoreEmulatorInfo = emulators[Emulators.FIRESTORE]; | ||
|
||
if (!firestoreEmulatorInfo) { | ||
throw Error( | ||
Check failure on line 21 in src/mcp/tools/firestore/emulator.ts
|
||
"No Firestore Emulator found running.", | ||
); | ||
} | ||
|
||
return `${firestoreEmulatorInfo.host}:${firestoreEmulatorInfo.port}`; | ||
} | ||
Check failure on line 27 in src/mcp/tools/firestore/emulator.ts
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(offline discussion)
Not a big fan of the
allowEmulator
boolean + the env override. Can do a simple refactor to change them to take aapiClient
override option.If the client wants to use emulator, it can override.