firebase-tools 14.15.1 → 14.16.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/lib/commands/dataconnect-sdk-generate.js +28 -24
- package/lib/commands/firestore-bulkdelete.js +73 -0
- package/lib/commands/firestore-operations-cancel.js +44 -0
- package/lib/commands/firestore-operations-describe.js +29 -0
- package/lib/commands/firestore-operations-list.js +29 -0
- package/lib/commands/firestore-utils.js +15 -0
- package/lib/commands/functions-config-export.js +5 -2
- package/lib/commands/index.js +5 -0
- package/lib/config.js +16 -4
- package/lib/dataconnect/ensureApis.js +3 -3
- package/lib/deploy/functions/build.js +2 -13
- package/lib/deploy/functions/deploy.js +4 -3
- package/lib/deploy/functions/prepare.js +10 -7
- package/lib/deploy/functions/runtimes/discovery/index.js +1 -1
- package/lib/emulator/auth/operations.js +10 -1
- package/lib/emulator/commandUtils.js +7 -1
- package/lib/emulator/controller.js +15 -31
- package/lib/emulator/dataconnectEmulator.js +27 -24
- package/lib/emulator/downloadableEmulatorInfo.json +18 -18
- package/lib/emulator/functionsEmulator.js +1 -1
- package/lib/emulator/hub.js +9 -5
- package/lib/extensions/runtimes/common.js +3 -2
- package/lib/firestore/api.js +45 -0
- package/lib/firestore/pretty-print.js +23 -0
- package/lib/functions/env.js +12 -1
- package/lib/functions/projectConfig.js +69 -9
- package/lib/gcp/cloudfunctions.js +1 -6
- package/lib/gcp/cloudfunctionsv2.js +1 -9
- package/lib/gcp/cloudsql/cloudsqladmin.js +2 -2
- package/lib/init/features/dataconnect/create_app.js +7 -2
- package/lib/init/features/dataconnect/index.js +72 -56
- package/lib/init/features/dataconnect/sdk.js +23 -11
- package/lib/mcp/errors.js +2 -10
- package/lib/mcp/index.js +1 -4
- package/lib/mcp/prompts/core/deploy.js +1 -1
- package/lib/mcp/prompts/crashlytics/connect.js +114 -0
- package/lib/mcp/prompts/crashlytics/index.js +2 -3
- package/lib/mcp/tools/auth/disable_user.js +1 -1
- package/lib/mcp/tools/auth/get_user.js +9 -2
- package/lib/mcp/tools/core/index.js +4 -0
- package/lib/mcp/tools/core/init.js +11 -2
- package/lib/mcp/tools/core/login.js +46 -0
- package/lib/mcp/tools/core/logout.js +62 -0
- package/lib/mcp/tools/dataconnect/execute.js +71 -0
- package/lib/mcp/tools/dataconnect/index.js +3 -13
- package/lib/mcp/tools/dataconnect/list_services.js +104 -7
- package/lib/mcp/util.js +1 -17
- package/lib/serve/functions.js +4 -3
- package/lib/track.js +16 -0
- package/lib/unzip.js +13 -0
- package/lib/utils.js +17 -1
- package/package.json +1 -1
- package/schema/firebase-config.json +160 -59
- package/lib/mcp/prompts/crashlytics/common.js +0 -10
- package/lib/mcp/prompts/crashlytics/fix_issue.js +0 -89
- package/lib/mcp/prompts/crashlytics/prioritize_issues.js +0 -79
- package/lib/mcp/tools/database/set_rules.js +0 -41
- package/lib/mcp/tools/dataconnect/execute_graphql.js +0 -48
- package/lib/mcp/tools/dataconnect/execute_graphql_read.js +0 -48
- package/lib/mcp/tools/dataconnect/execute_mutation.js +0 -62
- package/lib/mcp/tools/dataconnect/execute_query.js +0 -62
- package/lib/mcp/tools/dataconnect/get_connector.js +0 -31
- package/lib/mcp/tools/dataconnect/get_schema.js +0 -31
|
@@ -26,26 +26,25 @@ const CONNECTOR_YAML_TEMPLATE = (0, templates_1.readTemplateSync)("init/dataconn
|
|
|
26
26
|
const SCHEMA_TEMPLATE = (0, templates_1.readTemplateSync)("init/dataconnect/schema.gql");
|
|
27
27
|
const QUERIES_TEMPLATE = (0, templates_1.readTemplateSync)("init/dataconnect/queries.gql");
|
|
28
28
|
const MUTATIONS_TEMPLATE = (0, templates_1.readTemplateSync)("init/dataconnect/mutations.gql");
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
files: [],
|
|
33
|
-
};
|
|
34
|
-
const defaultConnector = {
|
|
35
|
-
id: "example",
|
|
36
|
-
path: "./example",
|
|
37
|
-
files: [
|
|
38
|
-
{
|
|
39
|
-
path: "queries.gql",
|
|
40
|
-
content: QUERIES_TEMPLATE,
|
|
41
|
-
},
|
|
29
|
+
const templateServiceInfo = {
|
|
30
|
+
schemaGql: [{ path: "schema.gql", content: SCHEMA_TEMPLATE }],
|
|
31
|
+
connectors: [
|
|
42
32
|
{
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
id: "example",
|
|
34
|
+
path: "./example",
|
|
35
|
+
files: [
|
|
36
|
+
{
|
|
37
|
+
path: "queries.gql",
|
|
38
|
+
content: QUERIES_TEMPLATE,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
path: "mutations.gql",
|
|
42
|
+
content: MUTATIONS_TEMPLATE,
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
45
|
},
|
|
46
46
|
],
|
|
47
47
|
};
|
|
48
|
-
const defaultSchema = { path: "schema.gql", content: SCHEMA_TEMPLATE };
|
|
49
48
|
async function askQuestions(setup) {
|
|
50
49
|
const info = {
|
|
51
50
|
analyticsFlow: "cli",
|
|
@@ -54,6 +53,7 @@ async function askQuestions(setup) {
|
|
|
54
53
|
locationId: "",
|
|
55
54
|
cloudSqlInstanceId: "",
|
|
56
55
|
cloudSqlDatabase: "",
|
|
56
|
+
shouldProvisionCSQL: false,
|
|
57
57
|
};
|
|
58
58
|
if (setup.projectId) {
|
|
59
59
|
const hasBilling = await (0, cloudbilling_1.isBillingEnabled)(setup);
|
|
@@ -101,6 +101,7 @@ async function actuate(setup, config, options) {
|
|
|
101
101
|
void (0, track_1.trackGA4)("dataconnect_init", {
|
|
102
102
|
project_status: setup.projectId ? (setup.isBillingEnabled ? "blaze" : "spark") : "missing",
|
|
103
103
|
flow: info.analyticsFlow,
|
|
104
|
+
provision_cloud_sql: String(info.shouldProvisionCSQL),
|
|
104
105
|
});
|
|
105
106
|
}
|
|
106
107
|
if (info.appDescription) {
|
|
@@ -118,10 +119,11 @@ async function actuateWithInfo(setup, config, info, options) {
|
|
|
118
119
|
const projectId = setup.projectId;
|
|
119
120
|
if (!projectId) {
|
|
120
121
|
info.analyticsFlow += "_save_template";
|
|
121
|
-
return await writeFiles(config, info,
|
|
122
|
+
return await writeFiles(config, info, templateServiceInfo, options);
|
|
122
123
|
}
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
await (0, ensureApis_1.ensureApis)(projectId, true);
|
|
125
|
+
const provisionCSQL = info.shouldProvisionCSQL && (await (0, cloudbilling_1.isBillingEnabled)(setup));
|
|
126
|
+
if (provisionCSQL) {
|
|
125
127
|
await (0, provisionCloudSql_1.setupCloudSql)({
|
|
126
128
|
projectId: projectId,
|
|
127
129
|
location: info.locationId,
|
|
@@ -130,15 +132,18 @@ async function actuateWithInfo(setup, config, info, options) {
|
|
|
130
132
|
requireGoogleMlIntegration: false,
|
|
131
133
|
});
|
|
132
134
|
}
|
|
135
|
+
const serviceName = `projects/${projectId}/locations/${info.locationId}/services/${info.serviceId}`;
|
|
133
136
|
if (!info.appDescription) {
|
|
137
|
+
if (!info.serviceGql) {
|
|
138
|
+
await downloadService(info, serviceName);
|
|
139
|
+
}
|
|
134
140
|
if (info.serviceGql) {
|
|
135
141
|
info.analyticsFlow += "_save_downloaded";
|
|
136
142
|
return await writeFiles(config, info, info.serviceGql, options);
|
|
137
143
|
}
|
|
138
144
|
info.analyticsFlow += "_save_template";
|
|
139
|
-
return await writeFiles(config, info,
|
|
145
|
+
return await writeFiles(config, info, templateServiceInfo, options);
|
|
140
146
|
}
|
|
141
|
-
const serviceName = `projects/${projectId}/locations/${info.locationId}/services/${info.serviceId}`;
|
|
142
147
|
const serviceAlreadyExists = !(await (0, client_1.createService)(projectId, info.locationId, info.serviceId));
|
|
143
148
|
const schemaGql = await (0, utils_1.promiseWithSpinner)(() => (0, fdcExperience_1.generateSchema)(info.appDescription, projectId), "Generating the Data Connect Schema...");
|
|
144
149
|
const schemaFiles = [{ path: "schema.gql", content: schemaGql }];
|
|
@@ -148,7 +153,7 @@ async function actuateWithInfo(setup, config, info, options) {
|
|
|
148
153
|
return await writeFiles(config, info, { schemaGql: schemaFiles, connectors: [] }, options);
|
|
149
154
|
}
|
|
150
155
|
await (0, utils_1.promiseWithSpinner)(async () => {
|
|
151
|
-
const [saveSchemaGql, waitForCloudSQLProvision] = schemasDeploySequence(projectId, info, schemaFiles,
|
|
156
|
+
const [saveSchemaGql, waitForCloudSQLProvision] = schemasDeploySequence(projectId, info, schemaFiles, provisionCSQL);
|
|
152
157
|
await (0, client_1.upsertSchema)(saveSchemaGql);
|
|
153
158
|
if (waitForCloudSQLProvision) {
|
|
154
159
|
void (0, client_1.upsertSchema)(waitForCloudSQLProvision);
|
|
@@ -284,7 +289,6 @@ function subConnectorYamlValues(replacementValues) {
|
|
|
284
289
|
return replaced;
|
|
285
290
|
}
|
|
286
291
|
async function promptForExistingServices(setup, info) {
|
|
287
|
-
var _a, _b, _c, _d, _e;
|
|
288
292
|
if (!setup.projectId) {
|
|
289
293
|
return;
|
|
290
294
|
}
|
|
@@ -292,10 +296,7 @@ async function promptForExistingServices(setup, info) {
|
|
|
292
296
|
if (!existingServices.length) {
|
|
293
297
|
return;
|
|
294
298
|
}
|
|
295
|
-
const
|
|
296
|
-
return { service: s, schema: await (0, client_1.getSchema)(s.name) };
|
|
297
|
-
}));
|
|
298
|
-
const choice = await chooseExistingService(existingServicesAndSchemas);
|
|
299
|
+
const choice = await chooseExistingService(existingServices);
|
|
299
300
|
if (!choice) {
|
|
300
301
|
const existingServiceIds = existingServices.map((s) => s.name.split("/").pop());
|
|
301
302
|
info.serviceId = (0, utils_1.newUniqueId)(defaultServiceId(), existingServiceIds);
|
|
@@ -303,39 +304,50 @@ async function promptForExistingServices(setup, info) {
|
|
|
303
304
|
return;
|
|
304
305
|
}
|
|
305
306
|
info.analyticsFlow += "_pick_existing_service";
|
|
306
|
-
const serviceName = (0, names_1.parseServiceName)(choice.
|
|
307
|
+
const serviceName = (0, names_1.parseServiceName)(choice.name);
|
|
307
308
|
info.serviceId = serviceName.serviceId;
|
|
308
309
|
info.locationId = serviceName.location;
|
|
310
|
+
await downloadService(info, choice.name);
|
|
311
|
+
}
|
|
312
|
+
async function downloadService(info, serviceName) {
|
|
313
|
+
var _a, _b, _c, _d, _e;
|
|
314
|
+
const schema = await (0, client_1.getSchema)(serviceName);
|
|
315
|
+
if (!schema) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
309
318
|
info.serviceGql = {
|
|
310
319
|
schemaGql: [],
|
|
311
|
-
connectors: [
|
|
320
|
+
connectors: [
|
|
321
|
+
{
|
|
322
|
+
id: "example",
|
|
323
|
+
path: "./example",
|
|
324
|
+
files: [],
|
|
325
|
+
},
|
|
326
|
+
],
|
|
312
327
|
};
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
});
|
|
336
|
-
}
|
|
328
|
+
const primaryDatasource = schema.datasources.find((d) => d.postgresql);
|
|
329
|
+
if ((_b = (_a = primaryDatasource === null || primaryDatasource === void 0 ? void 0 : primaryDatasource.postgresql) === null || _a === void 0 ? void 0 : _a.cloudSql) === null || _b === void 0 ? void 0 : _b.instance) {
|
|
330
|
+
const instanceName = (0, names_1.parseCloudSQLInstanceName)(primaryDatasource.postgresql.cloudSql.instance);
|
|
331
|
+
info.cloudSqlInstanceId = instanceName.instanceId;
|
|
332
|
+
}
|
|
333
|
+
if ((_c = schema.source.files) === null || _c === void 0 ? void 0 : _c.length) {
|
|
334
|
+
info.serviceGql.schemaGql = schema.source.files;
|
|
335
|
+
}
|
|
336
|
+
info.cloudSqlDatabase = (_e = (_d = primaryDatasource === null || primaryDatasource === void 0 ? void 0 : primaryDatasource.postgresql) === null || _d === void 0 ? void 0 : _d.database) !== null && _e !== void 0 ? _e : "";
|
|
337
|
+
const connectors = await (0, client_1.listConnectors)(serviceName, [
|
|
338
|
+
"connectors.name",
|
|
339
|
+
"connectors.source.files",
|
|
340
|
+
]);
|
|
341
|
+
if (connectors.length) {
|
|
342
|
+
info.serviceGql.connectors = connectors.map((c) => {
|
|
343
|
+
const id = c.name.split("/").pop();
|
|
344
|
+
return {
|
|
345
|
+
id,
|
|
346
|
+
path: connectors.length === 1 ? "./example" : `./${id}`,
|
|
347
|
+
files: c.source.files || [],
|
|
348
|
+
};
|
|
349
|
+
});
|
|
337
350
|
}
|
|
338
|
-
return;
|
|
339
351
|
}
|
|
340
352
|
async function chooseExistingService(existing) {
|
|
341
353
|
const fdcConnector = (0, utils_1.envOverride)("FDC_CONNECTOR", "");
|
|
@@ -344,7 +356,7 @@ async function chooseExistingService(existing) {
|
|
|
344
356
|
if (serviceEnvVar) {
|
|
345
357
|
const [serviceLocationFromEnvVar, serviceIdFromEnvVar] = serviceEnvVar.split("/");
|
|
346
358
|
const serviceFromEnvVar = existing.find((s) => {
|
|
347
|
-
const serviceName = (0, names_1.parseServiceName)(s.
|
|
359
|
+
const serviceName = (0, names_1.parseServiceName)(s.name);
|
|
348
360
|
return (serviceName.serviceId === serviceIdFromEnvVar &&
|
|
349
361
|
serviceName.location === serviceLocationFromEnvVar);
|
|
350
362
|
});
|
|
@@ -356,7 +368,7 @@ async function chooseExistingService(existing) {
|
|
|
356
368
|
(0, utils_1.logWarning)(`Unable to pick up an existing service based on ${envVarName}=${serviceEnvVar}.`);
|
|
357
369
|
}
|
|
358
370
|
const choices = existing.map((s) => {
|
|
359
|
-
const serviceName = (0, names_1.parseServiceName)(s.
|
|
371
|
+
const serviceName = (0, names_1.parseServiceName)(s.name);
|
|
360
372
|
return {
|
|
361
373
|
name: `${serviceName.location}/${serviceName.serviceId}`,
|
|
362
374
|
value: s,
|
|
@@ -414,6 +426,10 @@ async function promptForCloudSQL(setup, info) {
|
|
|
414
426
|
choices,
|
|
415
427
|
default: "us-central1",
|
|
416
428
|
});
|
|
429
|
+
info.shouldProvisionCSQL = await (0, prompt_1.confirm)({
|
|
430
|
+
message: `Would you like to provision your Cloud SQL instance and database now?`,
|
|
431
|
+
default: true,
|
|
432
|
+
});
|
|
417
433
|
}
|
|
418
434
|
if (info.cloudSqlInstanceId !== "" && info.cloudSqlDatabase === "") {
|
|
419
435
|
try {
|
|
@@ -26,22 +26,30 @@ async function askQuestions(setup) {
|
|
|
26
26
|
};
|
|
27
27
|
info.apps = await chooseApp();
|
|
28
28
|
if (!info.apps.length) {
|
|
29
|
-
const
|
|
30
|
-
|
|
29
|
+
const npxMissingWarning = (0, utils_1.commandExistsSync)("npx")
|
|
30
|
+
? ""
|
|
31
|
+
: clc.yellow(" (you need to install Node.js first)");
|
|
32
|
+
const flutterMissingWarning = (0, utils_1.commandExistsSync)("flutter")
|
|
33
|
+
? ""
|
|
34
|
+
: clc.yellow(" (you need to install Flutter first)");
|
|
31
35
|
const choice = await (0, prompt_1.select)({
|
|
32
36
|
message: `Do you want to create an app template?`,
|
|
33
37
|
choices: [
|
|
34
|
-
{ name:
|
|
35
|
-
{ name:
|
|
38
|
+
{ name: `React${npxMissingWarning}`, value: "react" },
|
|
39
|
+
{ name: `Next.JS${npxMissingWarning}`, value: "next" },
|
|
40
|
+
{ name: `Flutter${flutterMissingWarning}`, value: "flutter" },
|
|
36
41
|
{ name: "no", value: "no" },
|
|
37
42
|
],
|
|
38
43
|
});
|
|
39
44
|
switch (choice) {
|
|
40
45
|
case "react":
|
|
41
|
-
await (0, create_app_1.createReactApp)(
|
|
46
|
+
await (0, create_app_1.createReactApp)((0, utils_1.newUniqueId)("web-app", (0, fsutils_1.listFiles)(cwd)));
|
|
42
47
|
break;
|
|
43
48
|
case "next":
|
|
44
|
-
await (0, create_app_1.createNextApp)(
|
|
49
|
+
await (0, create_app_1.createNextApp)((0, utils_1.newUniqueId)("web-app", (0, fsutils_1.listFiles)(cwd)));
|
|
50
|
+
break;
|
|
51
|
+
case "flutter":
|
|
52
|
+
await (0, create_app_1.createFlutterApp)((0, utils_1.newUniqueId)("flutter_app", (0, fsutils_1.listFiles)(cwd)));
|
|
45
53
|
break;
|
|
46
54
|
case "no":
|
|
47
55
|
break;
|
|
@@ -151,11 +159,15 @@ async function actuateWithInfo(setup, config, info) {
|
|
|
151
159
|
config.writeProjectFile(path.relative(config.projectDir, connectorYamlPath), connectorYamlContents);
|
|
152
160
|
(0, utils_1.logLabeledBullet)("dataconnect", `Installing the generated SDKs ...`);
|
|
153
161
|
const account = (0, auth_1.getGlobalDefaultAccount)();
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
162
|
+
try {
|
|
163
|
+
await dataconnectEmulator_1.DataConnectEmulator.generate({
|
|
164
|
+
configDir: connectorInfo.directory,
|
|
165
|
+
account,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
catch (e) {
|
|
169
|
+
(0, utils_1.logLabeledError)("dataconnect", `Failed to generate Data Connect SDKs\n${e === null || e === void 0 ? void 0 : e.message}`);
|
|
170
|
+
}
|
|
159
171
|
(0, utils_1.logLabeledSuccess)("dataconnect", `Installed generated SDKs for ${clc.bold(apps.map((a) => (0, appFinder_1.appDescription)(a)).join(", "))}`);
|
|
160
172
|
if (apps.some((a) => a.platform === types_1.Platform.IOS)) {
|
|
161
173
|
(0, utils_1.logBullet)(clc.bold("Please follow the instructions here to add your generated sdk to your XCode project:\n\thttps://firebase.google.com/docs/data-connect/ios-sdk#set-client"));
|
package/lib/mcp/errors.js
CHANGED
|
@@ -4,18 +4,10 @@ exports.mcpGeminiError = exports.mcpAuthError = exports.NO_PROJECT_ERROR = void
|
|
|
4
4
|
const util_1 = require("./util");
|
|
5
5
|
exports.NO_PROJECT_ERROR = (0, util_1.mcpError)('No active project was found. Use the `firebase_update_environment` tool to set the project directory to an absolute folder location containing a firebase.json config file. Alternatively, change the MCP server config to add [...,"--dir","/absolute/path/to/project/directory"] in its command-line arguments.', "PRECONDITION_FAILED");
|
|
6
6
|
function mcpAuthError(skipADC) {
|
|
7
|
-
const cmd = (0, util_1.commandExistsSync)("firebase") ? "firebase" : "npx -y firebase-tools";
|
|
8
7
|
if (skipADC) {
|
|
9
|
-
return (0, util_1.mcpError)(`The user is not currently logged into the Firebase CLI, which is required to use this tool. Please
|
|
10
|
-
\`\`\`sh
|
|
11
|
-
${cmd} login
|
|
12
|
-
\`\`\``);
|
|
8
|
+
return (0, util_1.mcpError)(`The user is not currently logged into the Firebase CLI, which is required to use this tool. Please run the 'firebase_login' tool to log in.`);
|
|
13
9
|
}
|
|
14
|
-
return (0, util_1.mcpError)(`The user is not currently logged into the Firebase CLI, which is required to use this tool. Please
|
|
15
|
-
\`\`\`sh
|
|
16
|
-
${cmd} login
|
|
17
|
-
\`\`\`
|
|
18
|
-
|
|
10
|
+
return (0, util_1.mcpError)(`The user is not currently logged into the Firebase CLI, which is required to use this tool. Please run the 'firebase_login' tool to log in, or instruct the user to configure [Application Default Credentials][ADC] on their machine.
|
|
19
11
|
[ADC]: https://cloud.google.com/docs/authentication/application-default-credentials`);
|
|
20
12
|
}
|
|
21
13
|
exports.mcpAuthError = mcpAuthError;
|
package/lib/mcp/index.js
CHANGED
|
@@ -140,9 +140,6 @@ class FirebaseMcpServer {
|
|
|
140
140
|
return this.emulatorHubClient;
|
|
141
141
|
}
|
|
142
142
|
const projectId = await this.getProjectId();
|
|
143
|
-
if (!projectId) {
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
143
|
this.emulatorHubClient = new hubClient_1.EmulatorHubClient(projectId);
|
|
147
144
|
return this.emulatorHubClient;
|
|
148
145
|
}
|
|
@@ -154,7 +151,7 @@ class FirebaseMcpServer {
|
|
|
154
151
|
const emulators = await hubClient.getEmulators();
|
|
155
152
|
const emulatorInfo = emulators[emulatorType];
|
|
156
153
|
if (!emulatorInfo) {
|
|
157
|
-
throw Error(
|
|
154
|
+
throw Error(`No ${emulatorType} Emulator found running. Make sure your project firebase.json file includes ${emulatorType} and then rerun emulator using \`firebase emulators:start\` from your project directory.`);
|
|
158
155
|
}
|
|
159
156
|
const host = emulatorInfo.host.includes(":") ? `[${emulatorInfo.host}]` : emulatorInfo.host;
|
|
160
157
|
return `http://${host}:${emulatorInfo.port}`;
|
|
@@ -83,7 +83,7 @@ Follow the steps below taking note of any user instructions provided above.
|
|
|
83
83
|
Create \`firebase.json\ with an "apphosting" configuration, setting backendId to the app's name in package.json: \`{"apphosting": {"backendId": "<backendId>"}}\
|
|
84
84
|
4b. If the app does NOT require SSR, configure Firebase Hosting:
|
|
85
85
|
Create \`firebase.json\ with a "hosting" configuration. Add a \`{"hosting": {"predeploy": "<build_script>"}}\` config to build before deploying.
|
|
86
|
-
5. Check if there is an active Firebase project for this environment (the \`firebase_get_environment\` tool may be helpful). If there is, proceed using that project. If there is not an active project, give the user two options:
|
|
86
|
+
5. Check if there is an active Firebase project for this environment (the \`firebase_get_environment\` tool may be helpful). If there is, provide the active project ID to the user and ask them if they want to proceed using that project. If there is not an active project, give the user two options: Provide an existing project ID or create a new project. Only use the list_projects tool on user request. Wait for their response before proceeding.
|
|
87
87
|
5a. If the user chooses to use an existing Firebase project, the \`firebase_list_projects\` tool may be helpful. Set the selected project as the active project (the \`firebase_update_environment\` tool may be helpful).
|
|
88
88
|
5b. If the user chooses to create a new project, use the \`firebase_create_project \` tool. Then set the new project as the active project (the \`firebase_update_environment\` tool may be helpful).
|
|
89
89
|
6. If firebase.json contains an "apphosting" configuration, check if a backend exists matching the provided backendId (the \`apphosting_list_backends\` tool may be helpful).
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.connect = void 0;
|
|
4
|
+
const prompt_1 = require("../../prompt");
|
|
5
|
+
exports.connect = (0, prompt_1.prompt)({
|
|
6
|
+
name: "connect",
|
|
7
|
+
omitPrefix: false,
|
|
8
|
+
description: "Access a Firebase application's Crashlytics data.",
|
|
9
|
+
annotations: {
|
|
10
|
+
title: "Access Crashlytics data",
|
|
11
|
+
},
|
|
12
|
+
}, async (unused, { accountEmail }) => {
|
|
13
|
+
return [
|
|
14
|
+
{
|
|
15
|
+
role: "user",
|
|
16
|
+
content: {
|
|
17
|
+
type: "text",
|
|
18
|
+
text: `
|
|
19
|
+
You are going to help a developer prioritize and fix issues in their
|
|
20
|
+
mobile application by accessing their Firebase Crashlytics data.
|
|
21
|
+
|
|
22
|
+
Active user: ${accountEmail || "<NONE>"}
|
|
23
|
+
|
|
24
|
+
## Required first steps! Absolutely required! Incredibly important!
|
|
25
|
+
|
|
26
|
+
1. **Make sure the user is logged in. No Crashlytics tools will work if the user is not logged in.**
|
|
27
|
+
a. Use the \`firebase_get_environment\` tool to verify that the user is logged in,
|
|
28
|
+
and find the active Firebase project.
|
|
29
|
+
b. If the Firebase 'Active user' is set to <NONE>, instruct the user to run \`firebase login\`
|
|
30
|
+
before continuing.
|
|
31
|
+
|
|
32
|
+
2. **Get the app_id for the Firebase application.**
|
|
33
|
+
a. If this is an Android app, read the mobilesdk_app_id value specified in the
|
|
34
|
+
google-services.json file. If there are multiple files or multiple app ids in a
|
|
35
|
+
single file, ask the user to choose one by providing a numbered list of all the package names.
|
|
36
|
+
b. If this is an iOS app, read the GOOGLE_APP_ID from GoogleService-Info.plist file.
|
|
37
|
+
If there are multiple files or multiple app ids in single file, ask the user to
|
|
38
|
+
choose one by providing a numbered list of all the bundle names.
|
|
39
|
+
c. If you can't find either of the above, just ask the user for the app id.
|
|
40
|
+
|
|
41
|
+
## Next steps
|
|
42
|
+
|
|
43
|
+
Once you have confirmed that the user is logged in to Firebase, and confirmed the
|
|
44
|
+
id for the application that they want to access, then you can ask the user what actions
|
|
45
|
+
they would like to perform. Here are some possibilities and instructions follow below:
|
|
46
|
+
|
|
47
|
+
1. Prioritize the most impactful stability issues
|
|
48
|
+
2. Diagnose and propose a fix for a crash
|
|
49
|
+
|
|
50
|
+
## Instructions for Using Crashlytics Data
|
|
51
|
+
|
|
52
|
+
### How to prioritize issues
|
|
53
|
+
|
|
54
|
+
Follow these steps to fetch issues and prioritize them.
|
|
55
|
+
|
|
56
|
+
1. Use the 'crashlytics_list_top_issues' tool to fetch up to 20 issues.
|
|
57
|
+
2. Use the 'crashlytics_list_top_versions' tool to fetch the top versions for this app.
|
|
58
|
+
3. If the user instructions include statements about prioritization, use those instructions.
|
|
59
|
+
4. If the user instructions do not include statements about prioritization,
|
|
60
|
+
then prioritize the returned issues using the following criteria:
|
|
61
|
+
4a. The app versions for the issue include the most recent version of the app.
|
|
62
|
+
4b. The number of users experiencing the issue across variants
|
|
63
|
+
4c. The volume of crashes
|
|
64
|
+
5. Return the top 5 issues, with a brief description each in a numerical list with the following format:
|
|
65
|
+
1. Issue <issue id>
|
|
66
|
+
* <the issue title>
|
|
67
|
+
* <the issue subtitle>
|
|
68
|
+
* **Description:** <a discription of the issue based on information from the tool response>
|
|
69
|
+
* **Rationale:** <the reason this issue was prioritized in the way it was>
|
|
70
|
+
|
|
71
|
+
### How to diagnose and fix issues
|
|
72
|
+
|
|
73
|
+
Follow these steps to diagnose and fix issues.
|
|
74
|
+
|
|
75
|
+
1. Make sure you have a good understanding of the code structure and where different functionality exists
|
|
76
|
+
2. Use the 'crashlytics_get_issue_details' tool to get more context on the issue.
|
|
77
|
+
3. Use the 'crashlytics_get_sample_crash_for_issue' tool to get 3 example crashes for this issue.
|
|
78
|
+
4. Read the files that exist in the stack trace of the issue to understand the crash deeply.
|
|
79
|
+
5. Determine the root cause of the crash.
|
|
80
|
+
6. Write out a plan using the following criteria:
|
|
81
|
+
6a. Write out a description of the issue and including
|
|
82
|
+
* A brief description of the cause of the issue
|
|
83
|
+
* A determination of your level of confidence in the cause of the issue
|
|
84
|
+
* A determination of which library is at fault, this codebase or a dependent library
|
|
85
|
+
* A determination for how complex the fix will be
|
|
86
|
+
6b. The plan should include relevant files to change
|
|
87
|
+
6c. The plan should include a test plan to verify the fix
|
|
88
|
+
6d. Use the following format for the plan:
|
|
89
|
+
|
|
90
|
+
## Cause
|
|
91
|
+
<A description of the root cause leading to the issue>
|
|
92
|
+
- **Fault**: <a determination of whether this code base is at fault or a dependent library is at fault>
|
|
93
|
+
- **Complexity**: <one of "simple", "moderately simple", "moderately hard", "hard", "oof, I don't know where to start">
|
|
94
|
+
|
|
95
|
+
## Fix
|
|
96
|
+
<A description of the fix for this issue and a break down of the changes.>
|
|
97
|
+
1. <Step 1>
|
|
98
|
+
2. <Step 2>
|
|
99
|
+
|
|
100
|
+
## Test
|
|
101
|
+
<A plan for how to test that the issue has been fixed and protect against regressions>
|
|
102
|
+
1. <Test case 1>
|
|
103
|
+
2. <Test case 2>
|
|
104
|
+
|
|
105
|
+
7. Present the plan to the user and get approval before making the change.
|
|
106
|
+
8. Fix the issue.
|
|
107
|
+
8a. Be mindful of API contracts and do not add fields to resources without a clear way to populate those fields
|
|
108
|
+
8b. If there is not enough information in the crash report to find a root cause, describe why you cannot fix the issue instead of making a guess.
|
|
109
|
+
9. Ask the developer if they would like you to test the fix for them.
|
|
110
|
+
`.trim(),
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.crashlyticsPrompts = void 0;
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
exports.crashlyticsPrompts = [fix_issue_1.fix_issue, prioritize_issues_1.prioritize_issues];
|
|
4
|
+
const connect_1 = require("./connect");
|
|
5
|
+
exports.crashlyticsPrompts = [connect_1.connect];
|
|
@@ -24,7 +24,7 @@ exports.disable_user = (0, tool_1.tool)({
|
|
|
24
24
|
}, async ({ uid, disabled }, { projectId }) => {
|
|
25
25
|
const res = await (0, auth_1.disableUser)(projectId, uid, disabled);
|
|
26
26
|
if (res) {
|
|
27
|
-
return (0, util_1.toContent)(`User ${uid}
|
|
27
|
+
return (0, util_1.toContent)(`User ${uid} has been ${disabled ? "disabled" : "enabled"}`);
|
|
28
28
|
}
|
|
29
29
|
return (0, util_1.toContent)(`Failed to ${disabled ? "disable" : "enable"} user ${uid}`);
|
|
30
30
|
});
|
|
@@ -32,7 +32,14 @@ exports.get_user = (0, tool_1.tool)({
|
|
|
32
32
|
},
|
|
33
33
|
}, async ({ email, phone_number, uid }, { projectId }) => {
|
|
34
34
|
if (email === undefined && phone_number === undefined && uid === undefined) {
|
|
35
|
-
return (0, util_1.mcpError)(
|
|
35
|
+
return (0, util_1.mcpError)("No user identifier supplied in auth_get_user tool");
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
let user;
|
|
38
|
+
try {
|
|
39
|
+
user = await (0, auth_1.findUser)(projectId, email, phone_number, uid);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
return (0, util_1.mcpError)("Unable to find user");
|
|
43
|
+
}
|
|
44
|
+
return (0, util_1.toContent)(user);
|
|
38
45
|
});
|
|
@@ -13,7 +13,11 @@ const get_environment_1 = require("./get_environment");
|
|
|
13
13
|
const update_environment_1 = require("./update_environment");
|
|
14
14
|
const list_projects_1 = require("./list_projects");
|
|
15
15
|
const consult_assistant_1 = require("./consult_assistant");
|
|
16
|
+
const login_1 = require("./login");
|
|
17
|
+
const logout_1 = require("./logout");
|
|
16
18
|
exports.coreTools = [
|
|
19
|
+
login_1.login,
|
|
20
|
+
logout_1.logout,
|
|
17
21
|
get_project_1.get_project,
|
|
18
22
|
list_apps_1.list_apps,
|
|
19
23
|
get_admin_sdk_config_1.get_admin_sdk_config,
|
|
@@ -6,6 +6,7 @@ const tool_1 = require("../../tool");
|
|
|
6
6
|
const util_1 = require("../../util");
|
|
7
7
|
const database_1 = require("../../../init/features/database");
|
|
8
8
|
const index_1 = require("../../../init/index");
|
|
9
|
+
const freeTrial_1 = require("../../../dataconnect/freeTrial");
|
|
9
10
|
exports.init = (0, tool_1.tool)({
|
|
10
11
|
name: "init",
|
|
11
12
|
description: "Initializes selected Firebase features in the workspace (Firestore, Data Connect, Realtime Database). All features are optional; provide only the products you wish to set up. " +
|
|
@@ -70,12 +71,19 @@ exports.init = (0, tool_1.tool)({
|
|
|
70
71
|
cloudsql_instance_id: zod_1.z
|
|
71
72
|
.string()
|
|
72
73
|
.optional()
|
|
73
|
-
.describe("The GCP Cloud SQL instance ID to use in the Firebase Data Connect service. By default, use <serviceId>-fdc."
|
|
74
|
+
.describe("The GCP Cloud SQL instance ID to use in the Firebase Data Connect service. By default, use <serviceId>-fdc. " +
|
|
75
|
+
"\nSet `provision_cloudsql` to true to start Cloud SQL provisioning."),
|
|
74
76
|
cloudsql_database: zod_1.z
|
|
75
77
|
.string()
|
|
76
78
|
.optional()
|
|
77
79
|
.default("fdcdb")
|
|
78
80
|
.describe("The Postgres database ID to use in the Firebase Data Connect service."),
|
|
81
|
+
provision_cloudsql: zod_1.z
|
|
82
|
+
.boolean()
|
|
83
|
+
.optional()
|
|
84
|
+
.default(false)
|
|
85
|
+
.describe("If true, provision the Cloud SQL instance if `cloudsql_instance_id` does not exist already. " +
|
|
86
|
+
`\nThe first Cloud SQL instance in the project will use the Data Connect no-cost trial. See its terms of service: ${(0, freeTrial_1.freeTrialTermsLink)()}.`),
|
|
79
87
|
})
|
|
80
88
|
.optional()
|
|
81
89
|
.describe("Provide this object to initialize Firebase Data Connect with Cloud SQL Postgres in this project directory.\n" +
|
|
@@ -138,6 +146,7 @@ exports.init = (0, tool_1.tool)({
|
|
|
138
146
|
locationId: features.dataconnect.location_id || "",
|
|
139
147
|
cloudSqlInstanceId: features.dataconnect.cloudsql_instance_id || "",
|
|
140
148
|
cloudSqlDatabase: features.dataconnect.cloudsql_database || "",
|
|
149
|
+
shouldProvisionCSQL: !!features.dataconnect.provision_cloudsql,
|
|
141
150
|
};
|
|
142
151
|
featureInfo.dataconnectSdk = {
|
|
143
152
|
apps: [],
|
|
@@ -155,7 +164,7 @@ exports.init = (0, tool_1.tool)({
|
|
|
155
164
|
config.writeProjectFile("firebase.json", setup.config);
|
|
156
165
|
config.writeProjectFile(".firebaserc", setup.rcfile);
|
|
157
166
|
if (featureInfo.dataconnectSdk && !featureInfo.dataconnectSdk.apps.length) {
|
|
158
|
-
setup.instructions.push(`No app is found in the current folder. We recommend you create an app (web, ios, android) first, then re-run the 'firebase_init' MCP tool to add Data Connect SDKs to your apps.
|
|
167
|
+
setup.instructions.push(`No app is found in the current folder. We recommend you create an app (web, ios, android) first, then re-run the 'firebase_init' MCP tool with the same input without app_description to add Data Connect SDKs to your apps.
|
|
159
168
|
Consider popular commands like 'npx create-react-app my-app', 'npx create-next-app my-app', 'flutter create my-app', etc`);
|
|
160
169
|
}
|
|
161
170
|
return (0, util_1.toContent)(`Successfully setup those features: ${featuresList.join(", ")}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.login = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const tool_1 = require("../../tool");
|
|
6
|
+
const auth_1 = require("../../../auth");
|
|
7
|
+
const util_1 = require("../../util");
|
|
8
|
+
const LoginInputSchema = zod_1.z.object({
|
|
9
|
+
authCode: zod_1.z.string().optional().describe("The authorization code from the login flow"),
|
|
10
|
+
});
|
|
11
|
+
exports.login = (0, tool_1.tool)({
|
|
12
|
+
name: "login",
|
|
13
|
+
description: "Logs the user into the Firebase CLI and MCP server.",
|
|
14
|
+
inputSchema: LoginInputSchema,
|
|
15
|
+
_meta: {
|
|
16
|
+
requiresAuth: false,
|
|
17
|
+
},
|
|
18
|
+
}, async (input, ctx) => {
|
|
19
|
+
const { authCode } = input;
|
|
20
|
+
const serverWithState = ctx.host;
|
|
21
|
+
if (authCode) {
|
|
22
|
+
if (!serverWithState.authorize) {
|
|
23
|
+
return (0, util_1.mcpError)("Login flow not started. Please call this tool without the authCode argument first to get a login URI.");
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const creds = await serverWithState.authorize(authCode);
|
|
27
|
+
delete serverWithState.authorize;
|
|
28
|
+
const user = creds.user;
|
|
29
|
+
return (0, util_1.toContent)(`Successfully logged in as ${user.email}`);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
delete serverWithState.authorize;
|
|
33
|
+
return (0, util_1.mcpError)(`Login failed: ${e.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const prototyper = await (0, auth_1.loginPrototyper)();
|
|
38
|
+
serverWithState.authorize = prototyper.authorize;
|
|
39
|
+
const result = {
|
|
40
|
+
uri: prototyper.uri,
|
|
41
|
+
sessionId: prototyper.sessionId,
|
|
42
|
+
};
|
|
43
|
+
const humanReadable = `Please visit this URL to login: ${result.uri}\nYour session ID is: ${result.sessionId}\nInstruct the use to copy the authorization code from that link, and paste it into chat.\nThen, run this tool again with that as the authCode argument to complete the login.`;
|
|
44
|
+
return (0, util_1.toContent)(humanReadable);
|
|
45
|
+
}
|
|
46
|
+
});
|