@theihtisham/mcp-server-firebase 1.0.0
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.
- package/LICENSE +21 -0
- package/README.md +362 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +79 -0
- package/dist/services/firebase.d.ts +14 -0
- package/dist/services/firebase.js +163 -0
- package/dist/tools/auth.d.ts +3 -0
- package/dist/tools/auth.js +346 -0
- package/dist/tools/firestore.d.ts +3 -0
- package/dist/tools/firestore.js +802 -0
- package/dist/tools/functions.d.ts +3 -0
- package/dist/tools/functions.js +168 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.js +30 -0
- package/dist/tools/messaging.d.ts +3 -0
- package/dist/tools/messaging.js +296 -0
- package/dist/tools/realtime-db.d.ts +4 -0
- package/dist/tools/realtime-db.js +271 -0
- package/dist/tools/storage.d.ts +3 -0
- package/dist/tools/storage.js +279 -0
- package/dist/tools/types.d.ts +11 -0
- package/dist/tools/types.js +3 -0
- package/dist/utils/cache.d.ts +16 -0
- package/dist/utils/cache.js +75 -0
- package/dist/utils/errors.d.ts +15 -0
- package/dist/utils/errors.js +94 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.js +37 -0
- package/dist/utils/pagination.d.ts +28 -0
- package/dist/utils/pagination.js +75 -0
- package/dist/utils/validation.d.ts +22 -0
- package/dist/utils/validation.js +172 -0
- package/package.json +53 -0
- package/src/index.ts +94 -0
- package/src/services/firebase.ts +140 -0
- package/src/tools/auth.ts +375 -0
- package/src/tools/firestore.ts +931 -0
- package/src/tools/functions.ts +189 -0
- package/src/tools/index.ts +24 -0
- package/src/tools/messaging.ts +324 -0
- package/src/tools/realtime-db.ts +307 -0
- package/src/tools/storage.ts +314 -0
- package/src/tools/types.ts +10 -0
- package/src/utils/cache.ts +82 -0
- package/src/utils/errors.ts +110 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/pagination.ts +105 -0
- package/src/utils/validation.ts +212 -0
- package/tests/cache.test.ts +139 -0
- package/tests/errors.test.ts +132 -0
- package/tests/firebase-service.test.ts +46 -0
- package/tests/pagination.test.ts +26 -0
- package/tests/tools.test.ts +226 -0
- package/tests/validation.test.ts +216 -0
- package/tsconfig.json +26 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.realtimeDbTools = void 0;
|
|
4
|
+
exports.validateRtdbPath = validateRtdbPath;
|
|
5
|
+
const firebase_js_1 = require("../services/firebase.js");
|
|
6
|
+
const index_js_1 = require("../utils/index.js");
|
|
7
|
+
// ============================================================
|
|
8
|
+
// REALTIME DATABASE TOOLS
|
|
9
|
+
// ============================================================
|
|
10
|
+
function validateRtdbPath(path) {
|
|
11
|
+
const trimmed = path.trim();
|
|
12
|
+
if (trimmed.length === 0) {
|
|
13
|
+
throw new Error('Realtime Database path cannot be empty.');
|
|
14
|
+
}
|
|
15
|
+
if (!trimmed.startsWith('/')) {
|
|
16
|
+
throw new Error(`Invalid Realtime Database path: "${trimmed}". Path must start with "/".`);
|
|
17
|
+
}
|
|
18
|
+
if (trimmed.includes('//')) {
|
|
19
|
+
throw new Error(`Invalid Realtime Database path: "${trimmed}". Path must not contain "//".`);
|
|
20
|
+
}
|
|
21
|
+
if (trimmed.includes('..')) {
|
|
22
|
+
throw new Error(`Invalid Realtime Database path: "${trimmed}". Path must not contain "..".`);
|
|
23
|
+
}
|
|
24
|
+
const segments = trimmed.split('/').filter(Boolean);
|
|
25
|
+
for (const seg of segments) {
|
|
26
|
+
if (seg.includes('.')) {
|
|
27
|
+
throw new Error(`Invalid segment "${seg}" in path. Segments must not contain ".".`);
|
|
28
|
+
}
|
|
29
|
+
if (seg.includes('$')) {
|
|
30
|
+
throw new Error(`Invalid segment "${seg}" in path. Segments must not contain "$".`);
|
|
31
|
+
}
|
|
32
|
+
if (seg.includes('#')) {
|
|
33
|
+
throw new Error(`Invalid segment "${seg}" in path. Segments must not contain "#".`);
|
|
34
|
+
}
|
|
35
|
+
if (seg.includes('[') || seg.includes(']')) {
|
|
36
|
+
throw new Error(`Invalid segment "${seg}" in path. Segments must not contain "[" or "]".`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return trimmed;
|
|
40
|
+
}
|
|
41
|
+
exports.realtimeDbTools = [
|
|
42
|
+
// ── rtdb_get_data ─────────────────────────────────────
|
|
43
|
+
{
|
|
44
|
+
name: 'rtdb_get_data',
|
|
45
|
+
description: 'Read data from Firebase Realtime Database at a given path.',
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
path: { type: 'string', description: 'Database path (e.g., "/users/uid123")' },
|
|
50
|
+
},
|
|
51
|
+
required: ['path'],
|
|
52
|
+
},
|
|
53
|
+
handler: async (args) => {
|
|
54
|
+
try {
|
|
55
|
+
const path = validateRtdbPath(args['path']);
|
|
56
|
+
const db = (0, firebase_js_1.getRealtimeDb)();
|
|
57
|
+
const snapshot = await db.ref(path).once('value');
|
|
58
|
+
return (0, index_js_1.formatSuccess)({
|
|
59
|
+
path,
|
|
60
|
+
exists: snapshot.exists(),
|
|
61
|
+
data: snapshot.val(),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
(0, index_js_1.handleFirebaseError)(err, 'rtdb', 'get_data');
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
// ── rtdb_set_data ─────────────────────────────────────
|
|
70
|
+
{
|
|
71
|
+
name: 'rtdb_set_data',
|
|
72
|
+
description: 'Set data at a path in Realtime Database. Overwrites any existing data.',
|
|
73
|
+
inputSchema: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
path: { type: 'string', description: 'Database path' },
|
|
77
|
+
data: { description: 'Data to set (any JSON-compatible value)' },
|
|
78
|
+
},
|
|
79
|
+
required: ['path', 'data'],
|
|
80
|
+
},
|
|
81
|
+
handler: async (args) => {
|
|
82
|
+
try {
|
|
83
|
+
const path = validateRtdbPath(args['path']);
|
|
84
|
+
const data = args['data'];
|
|
85
|
+
const db = (0, firebase_js_1.getRealtimeDb)();
|
|
86
|
+
await db.ref(path).set(data);
|
|
87
|
+
return (0, index_js_1.formatSuccess)({
|
|
88
|
+
path,
|
|
89
|
+
message: `Data set successfully at "${path}".`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
(0, index_js_1.handleFirebaseError)(err, 'rtdb', 'set_data');
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
// ── rtdb_push_data ────────────────────────────────────
|
|
98
|
+
{
|
|
99
|
+
name: 'rtdb_push_data',
|
|
100
|
+
description: 'Push new data to a list in Realtime Database. Auto-generates a unique key.',
|
|
101
|
+
inputSchema: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
path: { type: 'string', description: 'Parent path for the new item' },
|
|
105
|
+
data: { description: 'Data to push' },
|
|
106
|
+
},
|
|
107
|
+
required: ['path', 'data'],
|
|
108
|
+
},
|
|
109
|
+
handler: async (args) => {
|
|
110
|
+
try {
|
|
111
|
+
const path = validateRtdbPath(args['path']);
|
|
112
|
+
const data = args['data'];
|
|
113
|
+
const db = (0, firebase_js_1.getRealtimeDb)();
|
|
114
|
+
const newRef = db.ref(path).push();
|
|
115
|
+
await newRef.set(data);
|
|
116
|
+
// Get the path string from the reference
|
|
117
|
+
const newPath = newRef.toString().replace(newRef.root.toString(), '');
|
|
118
|
+
return (0, index_js_1.formatSuccess)({
|
|
119
|
+
path: newPath,
|
|
120
|
+
key: newRef.key,
|
|
121
|
+
message: `Data pushed successfully. New key: "${newRef.key}".`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
(0, index_js_1.handleFirebaseError)(err, 'rtdb', 'push_data');
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
// ── rtdb_update_data ──────────────────────────────────
|
|
130
|
+
{
|
|
131
|
+
name: 'rtdb_update_data',
|
|
132
|
+
description: 'Update specific fields at a path without overwriting the entire location.',
|
|
133
|
+
inputSchema: {
|
|
134
|
+
type: 'object',
|
|
135
|
+
properties: {
|
|
136
|
+
path: { type: 'string', description: 'Database path to update' },
|
|
137
|
+
data: { type: 'object', description: 'Fields to update' },
|
|
138
|
+
},
|
|
139
|
+
required: ['path', 'data'],
|
|
140
|
+
},
|
|
141
|
+
handler: async (args) => {
|
|
142
|
+
try {
|
|
143
|
+
const path = validateRtdbPath(args['path']);
|
|
144
|
+
const data = args['data'];
|
|
145
|
+
const db = (0, firebase_js_1.getRealtimeDb)();
|
|
146
|
+
await db.ref(path).update(data);
|
|
147
|
+
return (0, index_js_1.formatSuccess)({
|
|
148
|
+
path,
|
|
149
|
+
updatedFields: Object.keys(data),
|
|
150
|
+
message: `Data updated successfully at "${path}".`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
(0, index_js_1.handleFirebaseError)(err, 'rtdb', 'update_data');
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
// ── rtdb_remove_data ──────────────────────────────────
|
|
159
|
+
{
|
|
160
|
+
name: 'rtdb_remove_data',
|
|
161
|
+
description: 'Remove data at a path in Realtime Database.',
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: {
|
|
165
|
+
path: { type: 'string', description: 'Database path to remove' },
|
|
166
|
+
},
|
|
167
|
+
required: ['path'],
|
|
168
|
+
},
|
|
169
|
+
handler: async (args) => {
|
|
170
|
+
try {
|
|
171
|
+
const path = validateRtdbPath(args['path']);
|
|
172
|
+
const db = (0, firebase_js_1.getRealtimeDb)();
|
|
173
|
+
await db.ref(path).remove();
|
|
174
|
+
return (0, index_js_1.formatSuccess)({
|
|
175
|
+
path,
|
|
176
|
+
message: `Data removed successfully at "${path}".`,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
(0, index_js_1.handleFirebaseError)(err, 'rtdb', 'remove_data');
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
// ── rtdb_query_data ───────────────────────────────────
|
|
185
|
+
{
|
|
186
|
+
name: 'rtdb_query_data',
|
|
187
|
+
description: 'Query data in Realtime Database with ordering and filtering.',
|
|
188
|
+
inputSchema: {
|
|
189
|
+
type: 'object',
|
|
190
|
+
properties: {
|
|
191
|
+
path: { type: 'string', description: 'Database path to query' },
|
|
192
|
+
orderBy: {
|
|
193
|
+
type: 'object',
|
|
194
|
+
properties: {
|
|
195
|
+
child: { type: 'string', description: 'Order by child key' },
|
|
196
|
+
key: { type: 'boolean', description: 'Order by key' },
|
|
197
|
+
value: { type: 'boolean', description: 'Order by value' },
|
|
198
|
+
},
|
|
199
|
+
description: 'Order specification (provide one of child, key, or value)',
|
|
200
|
+
},
|
|
201
|
+
startAt: { description: 'Start at this value (inclusive)' },
|
|
202
|
+
endAt: { description: 'End at this value (inclusive)' },
|
|
203
|
+
equalTo: { description: 'Filter to exact value' },
|
|
204
|
+
limitToFirst: { type: 'number', description: 'Limit to first N results' },
|
|
205
|
+
limitToLast: { type: 'number', description: 'Limit to last N results' },
|
|
206
|
+
},
|
|
207
|
+
required: ['path'],
|
|
208
|
+
},
|
|
209
|
+
handler: async (args) => {
|
|
210
|
+
try {
|
|
211
|
+
const path = validateRtdbPath(args['path']);
|
|
212
|
+
const orderBy = args['orderBy'];
|
|
213
|
+
const startAt = args['startAt'];
|
|
214
|
+
const endAt = args['endAt'];
|
|
215
|
+
const equalTo = args['equalTo'];
|
|
216
|
+
const limitToFirst = args['limitToFirst'];
|
|
217
|
+
const limitToLast = args['limitToLast'];
|
|
218
|
+
const db = (0, firebase_js_1.getRealtimeDb)();
|
|
219
|
+
let query = db.ref(path);
|
|
220
|
+
// Apply ordering
|
|
221
|
+
if (orderBy?.child) {
|
|
222
|
+
query = query.orderByChild(orderBy.child);
|
|
223
|
+
}
|
|
224
|
+
else if (orderBy?.key) {
|
|
225
|
+
query = query.orderByKey();
|
|
226
|
+
}
|
|
227
|
+
else if (orderBy?.value) {
|
|
228
|
+
query = query.orderByValue();
|
|
229
|
+
}
|
|
230
|
+
// Apply filters
|
|
231
|
+
if (equalTo !== undefined) {
|
|
232
|
+
query = query.equalTo(equalTo);
|
|
233
|
+
}
|
|
234
|
+
if (startAt !== undefined) {
|
|
235
|
+
query = query.startAt(startAt);
|
|
236
|
+
}
|
|
237
|
+
if (endAt !== undefined) {
|
|
238
|
+
query = query.endAt(endAt);
|
|
239
|
+
}
|
|
240
|
+
// Apply limits
|
|
241
|
+
if (limitToFirst !== undefined) {
|
|
242
|
+
if (limitToFirst < 1 || limitToFirst > 10000) {
|
|
243
|
+
throw new Error('limitToFirst must be between 1 and 10000.');
|
|
244
|
+
}
|
|
245
|
+
query = query.limitToFirst(limitToFirst);
|
|
246
|
+
}
|
|
247
|
+
if (limitToLast !== undefined) {
|
|
248
|
+
if (limitToLast < 1 || limitToLast > 10000) {
|
|
249
|
+
throw new Error('limitToLast must be between 1 and 10000.');
|
|
250
|
+
}
|
|
251
|
+
query = query.limitToLast(limitToLast);
|
|
252
|
+
}
|
|
253
|
+
const snapshot = await query.once('value');
|
|
254
|
+
const results = [];
|
|
255
|
+
snapshot.forEach((child) => {
|
|
256
|
+
results.push({ key: child.key, value: child.val() });
|
|
257
|
+
return false;
|
|
258
|
+
});
|
|
259
|
+
return (0, index_js_1.formatSuccess)({
|
|
260
|
+
path,
|
|
261
|
+
count: results.length,
|
|
262
|
+
data: results,
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
(0, index_js_1.handleFirebaseError)(err, 'rtdb', 'query_data');
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
];
|
|
271
|
+
//# sourceMappingURL=realtime-db.js.map
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.storageTools = void 0;
|
|
4
|
+
const firebase_js_1 = require("../services/firebase.js");
|
|
5
|
+
const index_js_1 = require("../utils/index.js");
|
|
6
|
+
// ============================================================
|
|
7
|
+
// STORAGE TOOLS
|
|
8
|
+
// ============================================================
|
|
9
|
+
exports.storageTools = [
|
|
10
|
+
// ── storage_upload_file ───────────────────────────────
|
|
11
|
+
{
|
|
12
|
+
name: 'storage_upload_file',
|
|
13
|
+
description: 'Upload a file to Firebase Cloud Storage. Provide base64-encoded file content.',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
path: { type: 'string', description: 'Destination path in Storage (e.g., "images/photo.jpg")' },
|
|
18
|
+
contentBase64: { type: 'string', description: 'Base64-encoded file content' },
|
|
19
|
+
contentType: { type: 'string', description: 'MIME type (e.g., "image/png"). Auto-detected if not provided.' },
|
|
20
|
+
metadata: { type: 'object', description: 'Additional metadata key-value pairs' },
|
|
21
|
+
public: { type: 'boolean', description: 'Make file publicly accessible (default: false)' },
|
|
22
|
+
},
|
|
23
|
+
required: ['path', 'contentBase64'],
|
|
24
|
+
},
|
|
25
|
+
handler: async (args) => {
|
|
26
|
+
try {
|
|
27
|
+
const destinationPath = (0, index_js_1.validateStoragePath)(args['path']);
|
|
28
|
+
const contentBase64 = args['contentBase64'].trim();
|
|
29
|
+
const contentType = args['contentType'];
|
|
30
|
+
const metadata = args['metadata'];
|
|
31
|
+
const makePublic = args['public'] || false;
|
|
32
|
+
if (!contentBase64) {
|
|
33
|
+
throw new Error('contentBase64 cannot be empty.');
|
|
34
|
+
}
|
|
35
|
+
const buffer = Buffer.from(contentBase64, 'base64');
|
|
36
|
+
const storage = (0, firebase_js_1.getStorage)();
|
|
37
|
+
const bucket = storage.bucket();
|
|
38
|
+
const file = bucket.file(destinationPath);
|
|
39
|
+
const uploadMetadata = {};
|
|
40
|
+
if (contentType) {
|
|
41
|
+
uploadMetadata.contentType = contentType;
|
|
42
|
+
}
|
|
43
|
+
if (metadata) {
|
|
44
|
+
uploadMetadata.metadata = metadata;
|
|
45
|
+
}
|
|
46
|
+
await file.save(buffer, {
|
|
47
|
+
metadata: uploadMetadata,
|
|
48
|
+
resumable: false,
|
|
49
|
+
validation: 'crc32c',
|
|
50
|
+
});
|
|
51
|
+
if (makePublic) {
|
|
52
|
+
await file.makePublic();
|
|
53
|
+
}
|
|
54
|
+
const [url] = await file.getSignedUrl({
|
|
55
|
+
action: 'read',
|
|
56
|
+
expires: Date.now() + 60 * 60 * 1000,
|
|
57
|
+
});
|
|
58
|
+
return (0, index_js_1.formatSuccess)({
|
|
59
|
+
path: destinationPath,
|
|
60
|
+
size: buffer.length,
|
|
61
|
+
contentType: contentType || 'application/octet-stream',
|
|
62
|
+
public: makePublic,
|
|
63
|
+
signedUrl: url,
|
|
64
|
+
message: `File uploaded to "${destinationPath}" (${buffer.length} bytes).`,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
(0, index_js_1.handleFirebaseError)(err, 'storage', 'upload_file');
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
// ── storage_download_file ─────────────────────────────
|
|
73
|
+
{
|
|
74
|
+
name: 'storage_download_file',
|
|
75
|
+
description: 'Download a file from Firebase Cloud Storage. Returns base64-encoded content.',
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
path: { type: 'string', description: 'File path in Storage' },
|
|
80
|
+
},
|
|
81
|
+
required: ['path'],
|
|
82
|
+
},
|
|
83
|
+
handler: async (args) => {
|
|
84
|
+
try {
|
|
85
|
+
const filePath = (0, index_js_1.validateStoragePath)(args['path']);
|
|
86
|
+
const storage = (0, firebase_js_1.getStorage)();
|
|
87
|
+
const bucket = storage.bucket();
|
|
88
|
+
const file = bucket.file(filePath);
|
|
89
|
+
const [exists] = await file.exists();
|
|
90
|
+
if (!exists) {
|
|
91
|
+
return (0, index_js_1.formatSuccess)({
|
|
92
|
+
exists: false,
|
|
93
|
+
path: filePath,
|
|
94
|
+
message: `File "${filePath}" does not exist.`,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const [buffer] = await file.download();
|
|
98
|
+
const [metadata] = await file.getMetadata();
|
|
99
|
+
return (0, index_js_1.formatSuccess)({
|
|
100
|
+
exists: true,
|
|
101
|
+
path: filePath,
|
|
102
|
+
size: buffer.length,
|
|
103
|
+
contentType: metadata.contentType || 'application/octet-stream',
|
|
104
|
+
updated: metadata.updated,
|
|
105
|
+
contentBase64: buffer.toString('base64'),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
(0, index_js_1.handleFirebaseError)(err, 'storage', 'download_file');
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
// ── storage_list_files ────────────────────────────────
|
|
114
|
+
{
|
|
115
|
+
name: 'storage_list_files',
|
|
116
|
+
description: 'List files in a Firebase Cloud Storage bucket. Optionally filter by prefix.',
|
|
117
|
+
inputSchema: {
|
|
118
|
+
type: 'object',
|
|
119
|
+
properties: {
|
|
120
|
+
prefix: { type: 'string', description: 'Filter files by prefix (e.g., "images/")' },
|
|
121
|
+
pageSize: { type: 'number', description: 'Maximum results (1-1000, default 100)' },
|
|
122
|
+
pageToken: { type: 'string', description: 'Pagination token from previous result' },
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
handler: async (args) => {
|
|
126
|
+
try {
|
|
127
|
+
const prefix = args['prefix'] || '';
|
|
128
|
+
const pageSize = Math.min(Math.max(args['pageSize'] || 100, 1), 1000);
|
|
129
|
+
const pageToken = args['pageToken'];
|
|
130
|
+
const storage = (0, firebase_js_1.getStorage)();
|
|
131
|
+
const bucket = storage.bucket();
|
|
132
|
+
const options = {
|
|
133
|
+
prefix,
|
|
134
|
+
maxResults: pageSize,
|
|
135
|
+
autoPaginate: false,
|
|
136
|
+
};
|
|
137
|
+
if (pageToken) {
|
|
138
|
+
options.pageToken = pageToken;
|
|
139
|
+
}
|
|
140
|
+
const [files, nextQuery] = await bucket.getFiles(options);
|
|
141
|
+
const fileList = files.map((f) => ({
|
|
142
|
+
name: f.name,
|
|
143
|
+
size: f.metadata.size ? parseInt(f.metadata.size, 10) : 0,
|
|
144
|
+
contentType: f.metadata.contentType,
|
|
145
|
+
updated: f.metadata.updated,
|
|
146
|
+
timeCreated: f.metadata.timeCreated,
|
|
147
|
+
}));
|
|
148
|
+
return (0, index_js_1.formatListResult)(fileList, nextQuery?.pageToken);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
(0, index_js_1.handleFirebaseError)(err, 'storage', 'list_files');
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
// ── storage_delete_file ───────────────────────────────
|
|
156
|
+
{
|
|
157
|
+
name: 'storage_delete_file',
|
|
158
|
+
description: 'Delete a file from Firebase Cloud Storage.',
|
|
159
|
+
inputSchema: {
|
|
160
|
+
type: 'object',
|
|
161
|
+
properties: {
|
|
162
|
+
path: { type: 'string', description: 'File path to delete' },
|
|
163
|
+
},
|
|
164
|
+
required: ['path'],
|
|
165
|
+
},
|
|
166
|
+
handler: async (args) => {
|
|
167
|
+
try {
|
|
168
|
+
const filePath = (0, index_js_1.validateStoragePath)(args['path']);
|
|
169
|
+
const storage = (0, firebase_js_1.getStorage)();
|
|
170
|
+
const bucket = storage.bucket();
|
|
171
|
+
const file = bucket.file(filePath);
|
|
172
|
+
const [exists] = await file.exists();
|
|
173
|
+
if (!exists) {
|
|
174
|
+
return (0, index_js_1.formatSuccess)({
|
|
175
|
+
success: false,
|
|
176
|
+
path: filePath,
|
|
177
|
+
message: `File "${filePath}" does not exist.`,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
await file.delete();
|
|
181
|
+
return (0, index_js_1.formatSuccess)({
|
|
182
|
+
path: filePath,
|
|
183
|
+
message: `File "${filePath}" deleted successfully.`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
(0, index_js_1.handleFirebaseError)(err, 'storage', 'delete_file');
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
// ── storage_get_signed_url ────────────────────────────
|
|
192
|
+
{
|
|
193
|
+
name: 'storage_get_signed_url',
|
|
194
|
+
description: 'Generate a signed URL for a file in Firebase Cloud Storage.',
|
|
195
|
+
inputSchema: {
|
|
196
|
+
type: 'object',
|
|
197
|
+
properties: {
|
|
198
|
+
path: { type: 'string', description: 'File path' },
|
|
199
|
+
action: { type: 'string', enum: ['read', 'write', 'delete'], description: 'Action for the signed URL (default: read)' },
|
|
200
|
+
expiresInMs: { type: 'number', description: 'URL expiration in milliseconds (default: 3600000 = 1 hour)' },
|
|
201
|
+
},
|
|
202
|
+
required: ['path'],
|
|
203
|
+
},
|
|
204
|
+
handler: async (args) => {
|
|
205
|
+
try {
|
|
206
|
+
const filePath = (0, index_js_1.validateStoragePath)(args['path']);
|
|
207
|
+
const action = args['action'] || 'read';
|
|
208
|
+
const expiresInMs = args['expiresInMs'] || 3600000;
|
|
209
|
+
if (expiresInMs < 60000) {
|
|
210
|
+
throw new Error('Signed URL must be valid for at least 60 seconds.');
|
|
211
|
+
}
|
|
212
|
+
if (expiresInMs > 7 * 24 * 3600000) {
|
|
213
|
+
throw new Error('Signed URL cannot be valid for more than 7 days.');
|
|
214
|
+
}
|
|
215
|
+
const storage = (0, firebase_js_1.getStorage)();
|
|
216
|
+
const bucket = storage.bucket();
|
|
217
|
+
const file = bucket.file(filePath);
|
|
218
|
+
const [exists] = await file.exists();
|
|
219
|
+
if (!exists) {
|
|
220
|
+
return (0, index_js_1.formatSuccess)({
|
|
221
|
+
exists: false,
|
|
222
|
+
path: filePath,
|
|
223
|
+
message: `File "${filePath}" does not exist.`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const [url] = await file.getSignedUrl({
|
|
227
|
+
action,
|
|
228
|
+
expires: Date.now() + expiresInMs,
|
|
229
|
+
});
|
|
230
|
+
return (0, index_js_1.formatSuccess)({
|
|
231
|
+
path: filePath,
|
|
232
|
+
action,
|
|
233
|
+
signedUrl: url,
|
|
234
|
+
expiresAt: new Date(Date.now() + expiresInMs).toISOString(),
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
(0, index_js_1.handleFirebaseError)(err, 'storage', 'get_signed_url');
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
// ── storage_get_metadata ──────────────────────────────
|
|
243
|
+
{
|
|
244
|
+
name: 'storage_get_metadata',
|
|
245
|
+
description: 'Get metadata for a file in Firebase Cloud Storage.',
|
|
246
|
+
inputSchema: {
|
|
247
|
+
type: 'object',
|
|
248
|
+
properties: {
|
|
249
|
+
path: { type: 'string', description: 'File path' },
|
|
250
|
+
},
|
|
251
|
+
required: ['path'],
|
|
252
|
+
},
|
|
253
|
+
handler: async (args) => {
|
|
254
|
+
try {
|
|
255
|
+
const filePath = (0, index_js_1.validateStoragePath)(args['path']);
|
|
256
|
+
const storage = (0, firebase_js_1.getStorage)();
|
|
257
|
+
const bucket = storage.bucket();
|
|
258
|
+
const file = bucket.file(filePath);
|
|
259
|
+
const [metadata] = await file.getMetadata();
|
|
260
|
+
return (0, index_js_1.formatSuccess)({
|
|
261
|
+
name: metadata.name,
|
|
262
|
+
bucket: metadata.bucket,
|
|
263
|
+
size: metadata.size ? parseInt(metadata.size, 10) : 0,
|
|
264
|
+
contentType: metadata.contentType,
|
|
265
|
+
timeCreated: metadata.timeCreated,
|
|
266
|
+
updated: metadata.updated,
|
|
267
|
+
storageClass: metadata.storageClass,
|
|
268
|
+
md5Hash: metadata.md5Hash,
|
|
269
|
+
crc32c: metadata.crc32c,
|
|
270
|
+
customMetadata: metadata.metadata || {},
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
(0, index_js_1.handleFirebaseError)(err, 'storage', 'get_metadata');
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
];
|
|
279
|
+
//# sourceMappingURL=storage.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ToolDefinition {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
inputSchema: {
|
|
5
|
+
type: 'object';
|
|
6
|
+
properties: Record<string, unknown>;
|
|
7
|
+
required?: string[];
|
|
8
|
+
};
|
|
9
|
+
handler: (args: Record<string, unknown>) => Promise<string>;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare class LRUCache<T> {
|
|
2
|
+
private cache;
|
|
3
|
+
private readonly maxSize;
|
|
4
|
+
private readonly defaultTtlMs;
|
|
5
|
+
constructor(maxSize?: number, defaultTtlMs?: number);
|
|
6
|
+
get(key: string): T | undefined;
|
|
7
|
+
set(key: string, value: T, ttlMs?: number): void;
|
|
8
|
+
delete(key: string): boolean;
|
|
9
|
+
clear(): void;
|
|
10
|
+
get size(): number;
|
|
11
|
+
has(key: string): boolean;
|
|
12
|
+
invalidatePrefix(prefix: string): number;
|
|
13
|
+
}
|
|
14
|
+
export declare const firestoreCache: LRUCache<unknown>;
|
|
15
|
+
export declare const schemaCache: LRUCache<unknown>;
|
|
16
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.schemaCache = exports.firestoreCache = exports.LRUCache = void 0;
|
|
4
|
+
class LRUCache {
|
|
5
|
+
cache = new Map();
|
|
6
|
+
maxSize;
|
|
7
|
+
defaultTtlMs;
|
|
8
|
+
constructor(maxSize = 200, defaultTtlMs = 5 * 60 * 1000) {
|
|
9
|
+
this.maxSize = maxSize;
|
|
10
|
+
this.defaultTtlMs = defaultTtlMs;
|
|
11
|
+
}
|
|
12
|
+
get(key) {
|
|
13
|
+
const entry = this.cache.get(key);
|
|
14
|
+
if (!entry)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (Date.now() > entry.expiresAt) {
|
|
17
|
+
this.cache.delete(key);
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
// Move to end (most recently used)
|
|
21
|
+
this.cache.delete(key);
|
|
22
|
+
this.cache.set(key, entry);
|
|
23
|
+
return entry.value;
|
|
24
|
+
}
|
|
25
|
+
set(key, value, ttlMs) {
|
|
26
|
+
// Remove if exists to update position
|
|
27
|
+
this.cache.delete(key);
|
|
28
|
+
// Evict oldest entries if at capacity
|
|
29
|
+
while (this.cache.size >= this.maxSize) {
|
|
30
|
+
const firstKey = this.cache.keys().next().value;
|
|
31
|
+
if (firstKey !== undefined) {
|
|
32
|
+
this.cache.delete(firstKey);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
this.cache.set(key, {
|
|
36
|
+
value,
|
|
37
|
+
expiresAt: Date.now() + (ttlMs ?? this.defaultTtlMs),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
delete(key) {
|
|
41
|
+
return this.cache.delete(key);
|
|
42
|
+
}
|
|
43
|
+
clear() {
|
|
44
|
+
this.cache.clear();
|
|
45
|
+
}
|
|
46
|
+
get size() {
|
|
47
|
+
return this.cache.size;
|
|
48
|
+
}
|
|
49
|
+
has(key) {
|
|
50
|
+
const entry = this.cache.get(key);
|
|
51
|
+
if (!entry)
|
|
52
|
+
return false;
|
|
53
|
+
if (Date.now() > entry.expiresAt) {
|
|
54
|
+
this.cache.delete(key);
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
// Invalidate keys matching a prefix (useful for collection-level cache invalidation)
|
|
60
|
+
invalidatePrefix(prefix) {
|
|
61
|
+
let count = 0;
|
|
62
|
+
for (const key of this.cache.keys()) {
|
|
63
|
+
if (key.startsWith(prefix)) {
|
|
64
|
+
this.cache.delete(key);
|
|
65
|
+
count++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return count;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.LRUCache = LRUCache;
|
|
72
|
+
// Singleton caches for the server
|
|
73
|
+
exports.firestoreCache = new LRUCache(500, 30_000); // 30s TTL for Firestore reads
|
|
74
|
+
exports.schemaCache = new LRUCache(50, 10 * 60 * 1000); // 10min TTL for schema inference
|
|
75
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface FirebaseErrorContext {
|
|
2
|
+
service: string;
|
|
3
|
+
operation: string;
|
|
4
|
+
suggestion?: string;
|
|
5
|
+
details?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class FirebaseToolError extends Error {
|
|
8
|
+
readonly context: FirebaseErrorContext;
|
|
9
|
+
constructor(message: string, context: FirebaseErrorContext);
|
|
10
|
+
toStructuredMessage(): string;
|
|
11
|
+
}
|
|
12
|
+
export declare function handleFirebaseError(error: unknown, service: string, operation: string): never;
|
|
13
|
+
export declare function formatSuccess(data: unknown, operation?: string): string;
|
|
14
|
+
export declare function formatListResult(items: unknown[], pageToken?: string, totalCount?: number): string;
|
|
15
|
+
//# sourceMappingURL=errors.d.ts.map
|