firestore-mcp-readonly 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/dist/config.d.ts +11 -0
  4. package/dist/config.js +37 -0
  5. package/dist/config.js.map +1 -0
  6. package/dist/db.d.ts +4 -0
  7. package/dist/db.js +49 -0
  8. package/dist/db.js.map +1 -0
  9. package/dist/errors.d.ts +9 -0
  10. package/dist/errors.js +62 -0
  11. package/dist/errors.js.map +1 -0
  12. package/dist/index.d.ts +2 -0
  13. package/dist/index.js +60 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/limiter.d.ts +12 -0
  16. package/dist/limiter.js +30 -0
  17. package/dist/limiter.js.map +1 -0
  18. package/dist/serializer.d.ts +18 -0
  19. package/dist/serializer.js +39 -0
  20. package/dist/serializer.js.map +1 -0
  21. package/dist/tools/collection-group-query.d.ts +2 -0
  22. package/dist/tools/collection-group-query.js +80 -0
  23. package/dist/tools/collection-group-query.js.map +1 -0
  24. package/dist/tools/count-documents.d.ts +2 -0
  25. package/dist/tools/count-documents.js +54 -0
  26. package/dist/tools/count-documents.js.map +1 -0
  27. package/dist/tools/dump-database.d.ts +3 -0
  28. package/dist/tools/dump-database.js +89 -0
  29. package/dist/tools/dump-database.js.map +1 -0
  30. package/dist/tools/dump-node.d.ts +3 -0
  31. package/dist/tools/dump-node.js +107 -0
  32. package/dist/tools/dump-node.js.map +1 -0
  33. package/dist/tools/get-collection.d.ts +3 -0
  34. package/dist/tools/get-collection.js +53 -0
  35. package/dist/tools/get-collection.js.map +1 -0
  36. package/dist/tools/get-document.d.ts +2 -0
  37. package/dist/tools/get-document.js +52 -0
  38. package/dist/tools/get-document.js.map +1 -0
  39. package/dist/tools/get-server-info.d.ts +3 -0
  40. package/dist/tools/get-server-info.js +31 -0
  41. package/dist/tools/get-server-info.js.map +1 -0
  42. package/dist/tools/list-indexes.d.ts +3 -0
  43. package/dist/tools/list-indexes.js +92 -0
  44. package/dist/tools/list-indexes.js.map +1 -0
  45. package/dist/tools/list-subcollections.d.ts +2 -0
  46. package/dist/tools/list-subcollections.js +46 -0
  47. package/dist/tools/list-subcollections.js.map +1 -0
  48. package/dist/tools/query-collection.d.ts +2 -0
  49. package/dist/tools/query-collection.js +108 -0
  50. package/dist/tools/query-collection.js.map +1 -0
  51. package/dist/tools/read-collection-ordered.d.ts +3 -0
  52. package/dist/tools/read-collection-ordered.js +135 -0
  53. package/dist/tools/read-collection-ordered.js.map +1 -0
  54. package/package.json +70 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rilfi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # firestore-mcp-readonly
2
+
3
+ [![npm version](https://img.shields.io/npm/v/firestore-mcp-readonly)](https://www.npmjs.com/package/firestore-mcp-readonly)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Node.js >= 18](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
6
+
7
+ A read-only [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server for Google Cloud Firestore. Gives AI agents full read access to any Firestore database — query documents, explore subcollections, dump entire hierarchies — without any write capabilities.
8
+
9
+ ## Features
10
+
11
+ | Tool | Description |
12
+ |------|-------------|
13
+ | `firestore_get_document` | Get a single document by full path |
14
+ | `firestore_get_collection` | Get all documents from a collection |
15
+ | `firestore_query_collection` | Query with filters, ordering, pagination, cursors |
16
+ | `firestore_collection_group_query` | Query across all collections with the same ID |
17
+ | `firestore_list_subcollections` | List subcollection names under a document |
18
+ | `firestore_count_documents` | Count documents matching a query (aggregate) |
19
+ | `firestore_list_indexes` | List composite indexes in the database |
20
+ | `firestore_dump_node` | Recursively dump a subtree (documents + subcollections) |
21
+ | `firestore_dump_database` | Recursively dump the entire database |
22
+ | `firestore_read_collection_ordered` | Recursively read all documents from a collection and subcollections, returned as a flat sorted list |
23
+ | `firestore_get_server_info` | Get project ID, database ID, limits, root collections |
24
+
25
+ ### Query Operators
26
+
27
+ All Firestore query operators are supported:
28
+
29
+ `==`, `!=`, `<`, `<=`, `>`, `>=`, `array-contains`, `array-contains-any`, `in`, `not-in`
30
+
31
+ ### Pagination
32
+
33
+ - Offset-based: `limit` + `offset`
34
+ - Cursor-based: `startAt`, `startAfter`, `endAt`, `endBefore` (by document ID)
35
+
36
+ ### Type Serialization
37
+
38
+ Firestore-specific types are converted to JSON-safe objects:
39
+
40
+ | Firestore Type | JSON Output |
41
+ |---------------|-------------|
42
+ | Timestamp | `{ "_type": "timestamp", "value": "2024-01-01T00:00:00.000Z" }` |
43
+ | GeoPoint | `{ "_type": "geopoint", "lat": 37.7749, "lng": -122.4194 }` |
44
+ | DocumentReference | `{ "_type": "reference", "path": "users/alice" }` |
45
+ | Bytes | `{ "_type": "bytes", "base64": "..." }` |
46
+
47
+ ## Setup
48
+
49
+ ### Prerequisites
50
+
51
+ - Node.js >= 18
52
+ - A GCP service account key (see below)
53
+
54
+ ### Service Account Setup
55
+
56
+ 1. **Open the GCP Console**
57
+ Go to [IAM & Admin > Service Accounts](https://console.cloud.google.com/iam-admin/serviceaccounts) and select your project.
58
+
59
+ 2. **Create a service account**
60
+ Click **Create Service Account**. Use a descriptive name like `firestore-mcp-reader`. You can skip the optional description.
61
+
62
+ 3. **Assign roles**
63
+ On the "Grant this service account access to project" step, add:
64
+
65
+ | Role | Role ID | Purpose |
66
+ |------|---------|---------|
67
+ | Cloud Datastore Viewer | `roles/datastore.viewer` | **Required.** Read documents, query collections, list subcollections, count, and dump. |
68
+ | Cloud Datastore Index Admin | `roles/datastore.indexAdmin` | **Optional.** Only needed for the `firestore_list_indexes` tool. |
69
+
70
+ Click **Done** to finish creating the service account.
71
+
72
+ 4. **Create a JSON key**
73
+ Click on the newly created service account, go to the **Keys** tab, click **Add Key > Create new key**, select **JSON**, and click **Create**. A `.json` file will download automatically.
74
+
75
+ 5. **Store the key securely**
76
+ Move the downloaded file to a secure location (e.g. `~/.config/gcloud/firestore-mcp-reader.json`). This path goes into the `FIRESTORE_SERVICE_ACCOUNT_KEY_PATH` environment variable.
77
+
78
+ > **Do not commit the key file to version control.** Add it to your `.gitignore`.
79
+
80
+ ### Install globally (recommended)
81
+
82
+ ```bash
83
+ npm install -g firestore-mcp-readonly
84
+ ```
85
+
86
+ Then add to your MCP config (e.g. `claude_desktop_config.json`, `.mcp.json`):
87
+
88
+ ```json
89
+ {
90
+ "mcpServers": {
91
+ "firestore": {
92
+ "command": "firestore-mcp-readonly",
93
+ "env": {
94
+ "FIRESTORE_SERVICE_ACCOUNT_KEY_PATH": "/path/to/service-account.json",
95
+ "FIRESTORE_PROJECT_ID": "my-gcp-project",
96
+ "FIRESTORE_DATABASE_ID": "(default)",
97
+ "FIRESTORE_MAX_DOCS_PER_COLLECTION": "500",
98
+ "FIRESTORE_MAX_RECURSION_DEPTH": "10",
99
+ "FIRESTORE_MAX_TOTAL_DUMP_DOCS": "5000"
100
+ }
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+
107
+ ## Configuration
108
+
109
+ All configuration is via environment variables (set in the `env` block of `.mcp.json`):
110
+
111
+ | Variable | Required | Default | Description |
112
+ |----------|----------|---------|-------------|
113
+ | `FIRESTORE_SERVICE_ACCOUNT_KEY_PATH` | Yes | — | Absolute path to service account JSON key file |
114
+ | `FIRESTORE_PROJECT_ID` | Yes | — | GCP project ID |
115
+ | `FIRESTORE_DATABASE_ID` | No | `(default)` | Named database ID |
116
+ | `FIRESTORE_MAX_DOCS_PER_COLLECTION` | No | `500` | Max docs per single collection fetch |
117
+ | `FIRESTORE_MAX_RECURSION_DEPTH` | No | `10` | Max depth for recursive dumps |
118
+ | `FIRESTORE_MAX_TOTAL_DUMP_DOCS` | No | `5000` | Max total docs across a dump operation |
119
+
120
+ ## Safety
121
+
122
+ - **Read-only**: No create, update, or delete operations are exposed
123
+ - **Depth limits**: Recursive dumps stop at configurable depth
124
+ - **Document caps**: Total documents per dump are capped
125
+ - **Partial results**: When limits are hit, partial data is returned with `limitReached: true` instead of erroring
126
+ - **No secrets exposed**: `firestore_get_server_info` never returns the service account key
127
+
128
+ ## Contributing
129
+
130
+ Contributions are welcome! Please submit your pull requests to the `develop` branch. PRs targeting any other branch will not be accepted.
131
+
132
+ ### Branch Naming
133
+
134
+ - Features: `feature/FSMCP-<number>-<description>`
135
+ - Bug fixes: `bugfix/FSMCP-<number>-<description>`
136
+
137
+ ### Branch Flow
138
+
139
+ ```text
140
+ main ← release/x.x.x ← develop ← feature/FSMCP-xxx
141
+ ← bugfix/FSMCP-xxx
142
+ ```
143
+
144
+ ## License
145
+
146
+ MIT
@@ -0,0 +1,11 @@
1
+ export interface FirestoreConfig {
2
+ serviceAccountKeyPath: string;
3
+ projectId: string;
4
+ databaseId: string;
5
+ limits: {
6
+ maxDocsPerCollection: number;
7
+ maxRecursionDepth: number;
8
+ maxTotalDumpDocs: number;
9
+ };
10
+ }
11
+ export declare function loadConfig(): FirestoreConfig;
package/dist/config.js ADDED
@@ -0,0 +1,37 @@
1
+ import { existsSync } from 'fs';
2
+ export function loadConfig() {
3
+ const serviceAccountKeyPath = process.env.FIRESTORE_SERVICE_ACCOUNT_KEY_PATH;
4
+ const projectId = process.env.FIRESTORE_PROJECT_ID;
5
+ if (!serviceAccountKeyPath) {
6
+ throw new Error('FIRESTORE_SERVICE_ACCOUNT_KEY_PATH environment variable is required');
7
+ }
8
+ if (!projectId) {
9
+ throw new Error('FIRESTORE_PROJECT_ID environment variable is required');
10
+ }
11
+ if (!existsSync(serviceAccountKeyPath)) {
12
+ throw new Error(`Service account key file not found: ${serviceAccountKeyPath}`);
13
+ }
14
+ const maxDocsPerCollection = parseInt(process.env.FIRESTORE_MAX_DOCS_PER_COLLECTION || '500', 10);
15
+ const maxRecursionDepth = parseInt(process.env.FIRESTORE_MAX_RECURSION_DEPTH || '10', 10);
16
+ const maxTotalDumpDocs = parseInt(process.env.FIRESTORE_MAX_TOTAL_DUMP_DOCS || '5000', 10);
17
+ if (isNaN(maxDocsPerCollection)) {
18
+ throw new Error(`FIRESTORE_MAX_DOCS_PER_COLLECTION must be a number, got "${process.env.FIRESTORE_MAX_DOCS_PER_COLLECTION}"`);
19
+ }
20
+ if (isNaN(maxRecursionDepth)) {
21
+ throw new Error(`FIRESTORE_MAX_RECURSION_DEPTH must be a number, got "${process.env.FIRESTORE_MAX_RECURSION_DEPTH}"`);
22
+ }
23
+ if (isNaN(maxTotalDumpDocs)) {
24
+ throw new Error(`FIRESTORE_MAX_TOTAL_DUMP_DOCS must be a number, got "${process.env.FIRESTORE_MAX_TOTAL_DUMP_DOCS}"`);
25
+ }
26
+ return {
27
+ serviceAccountKeyPath,
28
+ projectId,
29
+ databaseId: process.env.FIRESTORE_DATABASE_ID || '(default)',
30
+ limits: {
31
+ maxDocsPerCollection,
32
+ maxRecursionDepth,
33
+ maxTotalDumpDocs,
34
+ },
35
+ };
36
+ }
37
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAahC,MAAM,UAAU,UAAU;IACxB,MAAM,qBAAqB,GAAG,OAAO,CAAC,GAAG,CAAC,kCAAkC,CAAC;IAC7E,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC;IAEnD,IAAI,CAAC,qBAAqB,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAC;IACzF,CAAC;IACD,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;IAC3E,CAAC;IACD,IAAI,CAAC,UAAU,CAAC,qBAAqB,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,uCAAuC,qBAAqB,EAAE,CAAC,CAAC;IAClF,CAAC;IAED,MAAM,oBAAoB,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,iCAAiC,IAAI,KAAK,EAAE,EAAE,CAAC,CAAC;IAClG,MAAM,iBAAiB,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,6BAA6B,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC;IAC1F,MAAM,gBAAgB,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,6BAA6B,IAAI,MAAM,EAAE,EAAE,CAAC,CAAC;IAE3F,IAAI,KAAK,CAAC,oBAAoB,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,KAAK,CAAC,4DAA4D,OAAO,CAAC,GAAG,CAAC,iCAAiC,GAAG,CAAC,CAAC;IAChI,CAAC;IACD,IAAI,KAAK,CAAC,iBAAiB,CAAC,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,wDAAwD,OAAO,CAAC,GAAG,CAAC,6BAA6B,GAAG,CAAC,CAAC;IACxH,CAAC;IACD,IAAI,KAAK,CAAC,gBAAgB,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,wDAAwD,OAAO,CAAC,GAAG,CAAC,6BAA6B,GAAG,CAAC,CAAC;IACxH,CAAC;IAED,OAAO;QACL,qBAAqB;QACrB,SAAS;QACT,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,qBAAqB,IAAI,WAAW;QAC5D,MAAM,EAAE;YACN,oBAAoB;YACpB,iBAAiB;YACjB,gBAAgB;SACjB;KACF,CAAC;AACJ,CAAC"}
package/dist/db.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { type Firestore } from 'firebase-admin/firestore';
2
+ import type { FirestoreConfig } from './config.js';
3
+ export declare function initDb(config: FirestoreConfig): void;
4
+ export declare function getDb(): Firestore;
package/dist/db.js ADDED
@@ -0,0 +1,49 @@
1
+ import { initializeApp, getApps, cert } from 'firebase-admin/app';
2
+ import { getFirestore } from 'firebase-admin/firestore';
3
+ import { readFileSync } from 'fs';
4
+ let db = null;
5
+ export function initDb(config) {
6
+ if (getApps().length === 0) {
7
+ let raw;
8
+ try {
9
+ raw = readFileSync(config.serviceAccountKeyPath, 'utf-8');
10
+ }
11
+ catch (err) {
12
+ throw new Error(`Failed to read service account key file at "${config.serviceAccountKeyPath}": ${err instanceof Error ? err.message : String(err)}`);
13
+ }
14
+ let serviceAccount;
15
+ try {
16
+ serviceAccount = JSON.parse(raw);
17
+ }
18
+ catch {
19
+ throw new Error(`Service account key file at "${config.serviceAccountKeyPath}" is not valid JSON`);
20
+ }
21
+ if (!serviceAccount.client_email || typeof serviceAccount.client_email !== 'string') {
22
+ throw new Error(`Service account key file is missing "client_email" field. Ensure you downloaded a JSON key (not a P12 key) from the GCP console.`);
23
+ }
24
+ if (!serviceAccount.private_key || typeof serviceAccount.private_key !== 'string') {
25
+ throw new Error(`Service account key file is missing "private_key" field. Ensure you downloaded a JSON key (not a P12 key) from the GCP console.`);
26
+ }
27
+ try {
28
+ initializeApp({
29
+ credential: cert(serviceAccount),
30
+ projectId: config.projectId,
31
+ });
32
+ }
33
+ catch (err) {
34
+ throw new Error(`Failed to initialize Firebase Admin with the service account at "${config.serviceAccountKeyPath}": ${err instanceof Error ? err.message : String(err)}`);
35
+ }
36
+ }
37
+ if (config.databaseId !== '(default)') {
38
+ db = getFirestore(config.databaseId);
39
+ }
40
+ else {
41
+ db = getFirestore();
42
+ }
43
+ }
44
+ export function getDb() {
45
+ if (!db)
46
+ throw new Error('Firestore not initialized — call initDb() first');
47
+ return db;
48
+ }
49
+ //# sourceMappingURL=db.js.map
package/dist/db.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAClE,OAAO,EAAE,YAAY,EAAkB,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAGlC,IAAI,EAAE,GAAqB,IAAI,CAAC;AAEhC,MAAM,UAAU,MAAM,CAAC,MAAuB;IAC5C,IAAI,OAAO,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,IAAI,GAAW,CAAC;QAChB,IAAI,CAAC;YACH,GAAG,GAAG,YAAY,CAAC,MAAM,CAAC,qBAAqB,EAAE,OAAO,CAAC,CAAC;QAC5D,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,+CAA+C,MAAM,CAAC,qBAAqB,MAAM,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACvJ,CAAC;QAED,IAAI,cAAuC,CAAC;QAC5C,IAAI,CAAC;YACH,cAAc,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA4B,CAAC;QAC9D,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,KAAK,CAAC,gCAAgC,MAAM,CAAC,qBAAqB,qBAAqB,CAAC,CAAC;QACrG,CAAC;QAED,IAAI,CAAC,cAAc,CAAC,YAAY,IAAI,OAAO,cAAc,CAAC,YAAY,KAAK,QAAQ,EAAE,CAAC;YACpF,MAAM,IAAI,KAAK,CAAC,kIAAkI,CAAC,CAAC;QACtJ,CAAC;QACD,IAAI,CAAC,cAAc,CAAC,WAAW,IAAI,OAAO,cAAc,CAAC,WAAW,KAAK,QAAQ,EAAE,CAAC;YAClF,MAAM,IAAI,KAAK,CAAC,iIAAiI,CAAC,CAAC;QACrJ,CAAC;QAED,IAAI,CAAC;YACH,aAAa,CAAC;gBACZ,UAAU,EAAE,IAAI,CAAC,cAA4C,CAAC;gBAC9D,SAAS,EAAE,MAAM,CAAC,SAAS;aAC5B,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,KAAK,CAAC,oEAAoE,MAAM,CAAC,qBAAqB,MAAM,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC5K,CAAC;IACH,CAAC;IAED,IAAI,MAAM,CAAC,UAAU,KAAK,WAAW,EAAE,CAAC;QACtC,EAAE,GAAG,YAAY,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IACvC,CAAC;SAAM,CAAC;QACN,EAAE,GAAG,YAAY,EAAE,CAAC;IACtB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,KAAK;IACnB,IAAI,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IAC5E,OAAO,EAAE,CAAC;AACZ,CAAC"}
@@ -0,0 +1,9 @@
1
+ interface FirestoreErrorInfo {
2
+ error: string;
3
+ code: string | null;
4
+ tool: string;
5
+ suggestion: string;
6
+ details: string | null;
7
+ }
8
+ export declare function formatFirestoreError(error: unknown, toolName: string): FirestoreErrorInfo;
9
+ export {};
package/dist/errors.js ADDED
@@ -0,0 +1,62 @@
1
+ const GRPC_CODE_MAP = {
2
+ 1: { name: 'CANCELLED', suggestion: 'The operation was cancelled. Retry the request.' },
3
+ 2: { name: 'UNKNOWN', suggestion: 'An unknown error occurred. Check the details for more information.' },
4
+ 3: { name: 'INVALID_ARGUMENT', suggestion: 'The request contains an invalid argument. Check the path, field names, and filter values.' },
5
+ 4: { name: 'DEADLINE_EXCEEDED', suggestion: 'The request timed out. Try reducing the scope (fewer documents, shallower depth).' },
6
+ 5: { name: 'NOT_FOUND', suggestion: 'The database or project was not found. Verify FIRESTORE_PROJECT_ID and FIRESTORE_DATABASE_ID are correct.' },
7
+ 6: { name: 'ALREADY_EXISTS', suggestion: 'The resource already exists.' },
8
+ 7: { name: 'PERMISSION_DENIED', suggestion: 'The service account lacks read access. Ensure it has the Cloud Datastore Viewer role (roles/datastore.viewer) in the GCP IAM console.' },
9
+ 8: { name: 'RESOURCE_EXHAUSTED', suggestion: 'Quota or rate limit exceeded. Reduce the request scope or wait before retrying.' },
10
+ 9: { name: 'FAILED_PRECONDITION', suggestion: 'This query requires a composite index. The error message above contains a direct link to create it in the Firebase console.' },
11
+ 10: { name: 'ABORTED', suggestion: 'The operation was aborted due to a conflict. Retry the request.' },
12
+ 13: { name: 'INTERNAL', suggestion: 'An internal Firestore error occurred. This is usually transient — retry the request.' },
13
+ 14: { name: 'UNAVAILABLE', suggestion: 'Firestore is temporarily unavailable. Retry the request after a short delay.' },
14
+ 16: { name: 'UNAUTHENTICATED', suggestion: 'The service account credentials are invalid or expired. Check that FIRESTORE_SERVICE_ACCOUNT_KEY_PATH points to a valid JSON key file.' },
15
+ };
16
+ function extractGrpcCode(error) {
17
+ if (typeof error === 'object' && error !== null && 'code' in error) {
18
+ const code = error.code;
19
+ if (typeof code === 'number')
20
+ return code;
21
+ }
22
+ return null;
23
+ }
24
+ function extractGrpcCodeName(error) {
25
+ const code = extractGrpcCode(error);
26
+ if (code !== null && GRPC_CODE_MAP[code]) {
27
+ return GRPC_CODE_MAP[code].name;
28
+ }
29
+ if (typeof error === 'object' && error !== null && 'code' in error) {
30
+ const code = error.code;
31
+ if (typeof code === 'string')
32
+ return code;
33
+ }
34
+ return null;
35
+ }
36
+ export function formatFirestoreError(error, toolName) {
37
+ const message = error instanceof Error ? error.message : String(error);
38
+ const grpcCode = extractGrpcCode(error);
39
+ const codeName = extractGrpcCodeName(error);
40
+ let suggestion = 'Check the error message for details. If this persists, verify your service account permissions and Firestore configuration.';
41
+ if (grpcCode !== null && GRPC_CODE_MAP[grpcCode]) {
42
+ suggestion = GRPC_CODE_MAP[grpcCode].suggestion;
43
+ }
44
+ let details = null;
45
+ if (error instanceof Error && error.stack) {
46
+ details = error.stack;
47
+ }
48
+ if (typeof error === 'object' && error !== null && 'details' in error) {
49
+ const d = error.details;
50
+ if (typeof d === 'string') {
51
+ details = d;
52
+ }
53
+ }
54
+ return {
55
+ error: message,
56
+ code: codeName,
57
+ tool: toolName,
58
+ suggestion,
59
+ details,
60
+ };
61
+ }
62
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ const require = createRequire(import.meta.url);
6
+ const { version } = require('../package.json');
7
+ import { loadConfig } from './config.js';
8
+ import { initDb } from './db.js';
9
+ import { registerGetDocument } from './tools/get-document.js';
10
+ import { registerGetCollection } from './tools/get-collection.js';
11
+ import { registerQueryCollection } from './tools/query-collection.js';
12
+ import { registerCollectionGroupQuery } from './tools/collection-group-query.js';
13
+ import { registerListSubcollections } from './tools/list-subcollections.js';
14
+ import { registerCountDocuments } from './tools/count-documents.js';
15
+ import { registerListIndexes } from './tools/list-indexes.js';
16
+ import { registerDumpNode } from './tools/dump-node.js';
17
+ import { registerDumpDatabase } from './tools/dump-database.js';
18
+ import { registerGetServerInfo } from './tools/get-server-info.js';
19
+ import { registerReadCollectionOrdered } from './tools/read-collection-ordered.js';
20
+ let config;
21
+ try {
22
+ config = loadConfig();
23
+ }
24
+ catch (err) {
25
+ const message = err instanceof Error ? err.message : String(err);
26
+ process.stderr.write(JSON.stringify({
27
+ error: `Configuration error: ${message}`,
28
+ suggestion: 'Check that FIRESTORE_SERVICE_ACCOUNT_KEY_PATH and FIRESTORE_PROJECT_ID environment variables are set correctly in your MCP config.',
29
+ }, null, 2) + '\n');
30
+ process.exit(1);
31
+ }
32
+ try {
33
+ initDb(config);
34
+ }
35
+ catch (err) {
36
+ const message = err instanceof Error ? err.message : String(err);
37
+ process.stderr.write(JSON.stringify({
38
+ error: `Database initialization error: ${message}`,
39
+ suggestion: 'Verify the service account key file is valid and the project ID matches your GCP project.',
40
+ }, null, 2) + '\n');
41
+ process.exit(1);
42
+ }
43
+ const server = new McpServer({
44
+ name: 'firestore-mcp-readonly',
45
+ version,
46
+ });
47
+ registerGetDocument(server);
48
+ registerGetCollection(server, config);
49
+ registerQueryCollection(server);
50
+ registerCollectionGroupQuery(server);
51
+ registerListSubcollections(server);
52
+ registerCountDocuments(server);
53
+ registerListIndexes(server, config);
54
+ registerDumpNode(server, config);
55
+ registerDumpDatabase(server, config);
56
+ registerGetServerInfo(server, config);
57
+ registerReadCollectionOrdered(server, config);
58
+ const transport = new StdioServerTransport();
59
+ await server.connect(transport);
60
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export declare class LimitExceededError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare class DumpLimiter {
5
+ private readonly maxTotal;
6
+ private readonly maxDepth;
7
+ private totalDocs;
8
+ constructor(maxTotal: number, maxDepth: number);
9
+ checkDepth(depth: number): void;
10
+ addDocs(count: number): void;
11
+ get docsProcessed(): number;
12
+ }
@@ -0,0 +1,30 @@
1
+ export class LimitExceededError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'LimitExceededError';
5
+ }
6
+ }
7
+ export class DumpLimiter {
8
+ maxTotal;
9
+ maxDepth;
10
+ totalDocs = 0;
11
+ constructor(maxTotal, maxDepth) {
12
+ this.maxTotal = maxTotal;
13
+ this.maxDepth = maxDepth;
14
+ }
15
+ checkDepth(depth) {
16
+ if (depth > this.maxDepth) {
17
+ throw new LimitExceededError(`Max recursion depth ${this.maxDepth} exceeded`);
18
+ }
19
+ }
20
+ addDocs(count) {
21
+ this.totalDocs += count;
22
+ if (this.totalDocs > this.maxTotal) {
23
+ throw new LimitExceededError(`Max total document count ${this.maxTotal} exceeded — dump truncated`);
24
+ }
25
+ }
26
+ get docsProcessed() {
27
+ return this.totalDocs;
28
+ }
29
+ }
30
+ //# sourceMappingURL=limiter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"limiter.js","sourceRoot":"","sources":["../src/limiter.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAC3C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAED,MAAM,OAAO,WAAW;IAIH;IACA;IAJX,SAAS,GAAG,CAAC,CAAC;IAEtB,YACmB,QAAgB,EAChB,QAAgB;QADhB,aAAQ,GAAR,QAAQ,CAAQ;QAChB,aAAQ,GAAR,QAAQ,CAAQ;IAChC,CAAC;IAEJ,UAAU,CAAC,KAAa;QACtB,IAAI,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC1B,MAAM,IAAI,kBAAkB,CAC1B,uBAAuB,IAAI,CAAC,QAAQ,WAAW,CAChD,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO,CAAC,KAAa;QACnB,IAAI,CAAC,SAAS,IAAI,KAAK,CAAC;QACxB,IAAI,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnC,MAAM,IAAI,kBAAkB,CAC1B,4BAA4B,IAAI,CAAC,QAAQ,4BAA4B,CACtE,CAAC;QACJ,CAAC;IACH,CAAC;IAED,IAAI,aAAa;QACf,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;CACF"}
@@ -0,0 +1,18 @@
1
+ export type SerializedValue = {
2
+ _type: 'timestamp';
3
+ value: string;
4
+ } | {
5
+ _type: 'geopoint';
6
+ lat: number;
7
+ lng: number;
8
+ } | {
9
+ _type: 'reference';
10
+ path: string;
11
+ } | {
12
+ _type: 'bytes';
13
+ base64: string;
14
+ } | null | boolean | number | string | SerializedValue[] | {
15
+ [key: string]: SerializedValue;
16
+ };
17
+ export declare function serializeValue(value: unknown): SerializedValue;
18
+ export declare function serializeDocument(id: string, path: string, data: Record<string, unknown>): Record<string, SerializedValue>;
@@ -0,0 +1,39 @@
1
+ import { Timestamp, GeoPoint, DocumentReference } from 'firebase-admin/firestore';
2
+ export function serializeValue(value) {
3
+ if (value === null || value === undefined)
4
+ return null;
5
+ if (value instanceof Timestamp) {
6
+ return { _type: 'timestamp', value: value.toDate().toISOString() };
7
+ }
8
+ if (value instanceof GeoPoint) {
9
+ return { _type: 'geopoint', lat: value.latitude, lng: value.longitude };
10
+ }
11
+ if (value instanceof DocumentReference) {
12
+ return { _type: 'reference', path: value.path };
13
+ }
14
+ if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
15
+ return { _type: 'bytes', base64: Buffer.from(value).toString('base64') };
16
+ }
17
+ if (Array.isArray(value)) {
18
+ return value.map(serializeValue);
19
+ }
20
+ if (typeof value === 'object') {
21
+ const out = {};
22
+ for (const [k, v] of Object.entries(value)) {
23
+ out[k] = serializeValue(v);
24
+ }
25
+ return out;
26
+ }
27
+ return value;
28
+ }
29
+ export function serializeDocument(id, path, data) {
30
+ const out = {
31
+ _id: id,
32
+ _path: path,
33
+ };
34
+ for (const [k, v] of Object.entries(data)) {
35
+ out[k] = serializeValue(v);
36
+ }
37
+ return out;
38
+ }
39
+ //# sourceMappingURL=serializer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serializer.js","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAclF,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAEvD,IAAI,KAAK,YAAY,SAAS,EAAE,CAAC;QAC/B,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC;IACrE,CAAC;IACD,IAAI,KAAK,YAAY,QAAQ,EAAE,CAAC;QAC9B,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,KAAK,CAAC,QAAQ,EAAE,GAAG,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC;IAC1E,CAAC;IACD,IAAI,KAAK,YAAY,iBAAiB,EAAE,CAAC;QACvC,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,CAAC;IAClD,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,YAAY,UAAU,EAAE,CAAC;QAC1D,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;IAC3E,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;IACnC,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,GAAG,GAAoC,EAAE,CAAC;QAChD,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,EAAE,CAAC;YACtE,GAAG,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC;QAC7B,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,OAAO,KAAkC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,EAAU,EACV,IAAY,EACZ,IAA6B;IAE7B,MAAM,GAAG,GAAoC;QAC3C,GAAG,EAAE,EAAE;QACP,KAAK,EAAE,IAAI;KACZ,CAAC;IACF,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,GAAG,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC;IAC7B,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerCollectionGroupQuery(server: McpServer): void;
@@ -0,0 +1,80 @@
1
+ import { z } from 'zod';
2
+ import { getDb } from '../db.js';
3
+ import { serializeDocument } from '../serializer.js';
4
+ import { formatFirestoreError } from '../errors.js';
5
+ const whereClause = z.object({
6
+ field: z.string().describe('Field name to filter on'),
7
+ op: z.enum(['==', '!=', '<', '<=', '>', '>=', 'array-contains', 'array-contains-any', 'in', 'not-in'])
8
+ .describe('Firestore comparison operator'),
9
+ value: z.union([
10
+ z.string(),
11
+ z.number(),
12
+ z.boolean(),
13
+ z.null(),
14
+ z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])),
15
+ ]).describe('Value to compare against'),
16
+ });
17
+ const orderByClause = z.object({
18
+ field: z.string().describe('Field name to order by'),
19
+ direction: z.enum(['asc', 'desc']).default('asc').describe('Sort direction'),
20
+ });
21
+ export function registerCollectionGroupQuery(server) {
22
+ server.tool('firestore_collection_group_query', 'Query across ALL collections with the same ID, regardless of their location in the document hierarchy. For example, querying collectionId "comments" will match /posts/p1/comments, /users/u1/comments, etc. Requires a collection group index.', {
23
+ collectionId: z.string().describe('Collection ID (not a full path) — e.g. "comments" matches all /*/comments/* documents'),
24
+ where: z.array(whereClause).optional()
25
+ .describe('Array of where filter clauses — all are ANDed together'),
26
+ orderBy: z.array(orderByClause).optional()
27
+ .describe('Array of orderBy clauses applied in order'),
28
+ limit: z.number().int().min(1).max(1000).optional().default(50)
29
+ .describe('Max documents to return (default: 50, max: 1000)'),
30
+ offset: z.number().int().min(0).optional().default(0)
31
+ .describe('Number of documents to skip'),
32
+ }, async ({ collectionId, where: whereClauses, orderBy: orderByClauses, limit, offset }) => {
33
+ try {
34
+ const db = getDb();
35
+ let query = db.collectionGroup(collectionId);
36
+ if (whereClauses) {
37
+ for (const clause of whereClauses) {
38
+ query = query.where(clause.field, clause.op, clause.value);
39
+ }
40
+ }
41
+ if (orderByClauses) {
42
+ for (const clause of orderByClauses) {
43
+ query = query.orderBy(clause.field, clause.direction);
44
+ }
45
+ }
46
+ if (offset) {
47
+ query = query.offset(offset);
48
+ }
49
+ query = query.limit(limit);
50
+ const snapshot = await query.get();
51
+ const documents = snapshot.docs.map(doc => serializeDocument(doc.id, doc.ref.path, doc.data()));
52
+ return {
53
+ content: [{
54
+ type: 'text',
55
+ text: JSON.stringify({
56
+ collectionId,
57
+ count: documents.length,
58
+ query: {
59
+ filters: whereClauses?.length ?? 0,
60
+ orderBy: orderByClauses?.map(o => `${o.field} ${o.direction}`) ?? [],
61
+ limit,
62
+ offset,
63
+ },
64
+ documents,
65
+ }, null, 2),
66
+ }],
67
+ };
68
+ }
69
+ catch (error) {
70
+ return {
71
+ content: [{
72
+ type: 'text',
73
+ text: JSON.stringify(formatFirestoreError(error, 'firestore_collection_group_query'), null, 2),
74
+ }],
75
+ isError: true,
76
+ };
77
+ }
78
+ });
79
+ }
80
+ //# sourceMappingURL=collection-group-query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection-group-query.js","sourceRoot":"","sources":["../../src/tools/collection-group-query.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerCountDocuments(server: McpServer): void;