firebase-tools 15.15.0 → 15.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/agentSkills.js +3 -2
- package/lib/apphosting/backend.js +7 -19
- package/lib/apphosting/localbuilds.js +32 -4
- package/lib/apphosting/prompts.js +32 -11
- package/lib/apphosting/secrets/index.js +43 -0
- package/lib/apphosting/yaml.js +2 -4
- package/lib/bin/firebase.js +10 -0
- package/lib/bin/mcp.js +12 -1
- package/lib/commands/apphosting-backends-create.js +4 -10
- package/lib/commands/dataconnect-sdk-generate.js +2 -2
- package/lib/commands/deploy.js +6 -1
- package/lib/config.js +1 -0
- package/lib/dataconnect/build.js +6 -0
- package/lib/dataconnect/names.js +1 -1
- package/lib/deploy/apphosting/prepare.js +5 -2
- package/lib/deploy/firestore/prepare.js +1 -1
- package/lib/deploy/functions/backend.js +22 -14
- package/lib/deploy/functions/prepare.js +8 -2
- package/lib/deploy/functions/release/fabricator.js +26 -14
- package/lib/deploy/functions/runtimes/python/index.js +3 -0
- package/lib/deploy/functions/runtimes/supported/types.js +6 -0
- package/lib/deploy/hosting/prepare.js +1 -0
- package/lib/emulator/adminSdkConfig.js +2 -1
- package/lib/emulator/apphosting/serve.js +2 -39
- package/lib/emulator/commandUtils.js +4 -1
- package/lib/emulator/controller.js +4 -3
- package/lib/emulator/dataconnectEmulator.js +7 -4
- package/lib/emulator/downloadableEmulatorInfo.json +30 -30
- package/lib/emulator/functionsEmulator.js +12 -6
- package/lib/emulator/functionsEmulatorRuntime.js +12 -5
- package/lib/emulator/functionsRuntimeWorker.js +6 -3
- package/lib/experiments.js +6 -1
- package/lib/frameworks/angular/index.js +6 -1
- package/lib/gcp/rules.js +8 -4
- package/lib/init/features/agentSkills.js +2 -2
- package/lib/init/features/apphosting.js +2 -8
- package/lib/init/features/dataconnect/index.js +26 -15
- package/lib/init/features/dataconnect/sdk.js +8 -3
- package/lib/init/features/firestore/rules.js +2 -1
- package/lib/init/features/functions/dart.js +2 -0
- package/lib/init/features/storage/rules.js +2 -1
- package/lib/mcp/apps/update_environment/mcp-app.js +138 -0
- package/lib/mcp/index.js +57 -2
- package/lib/mcp/resources/index.js +2 -0
- package/lib/mcp/resources/update_environment_ui.js +32 -0
- package/lib/mcp/tools/core/get_security_rules.js +2 -1
- package/lib/mcp/util.js +19 -0
- package/lib/rulesDeploy.js +35 -16
- package/lib/tsconfig.compile.tsbuildinfo +1 -1
- package/lib/tsconfig.publish.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/schema/firebase-config.json +4 -2
- package/templates/init/functions/dart/analysis_options.yaml +30 -0
- package/templates/init/functions/dart/pubspec.yaml +1 -0
|
@@ -26,11 +26,11 @@ const cloudbilling_1 = require("../../../gcp/cloudbilling");
|
|
|
26
26
|
exports.FDC_APP_FOLDER = "FDC_APP_FOLDER";
|
|
27
27
|
exports.FDC_SDK_FRAMEWORKS_ENV = "FDC_SDK_FRAMEWORKS";
|
|
28
28
|
exports.FDC_SDK_PLATFORM_ENV = "FDC_SDK_PLATFORM";
|
|
29
|
-
async function askQuestions(setup) {
|
|
29
|
+
async function askQuestions(setup, config, options) {
|
|
30
30
|
const info = {
|
|
31
31
|
apps: [],
|
|
32
32
|
};
|
|
33
|
-
info.apps = await chooseApp();
|
|
33
|
+
info.apps = await chooseApp(options);
|
|
34
34
|
if (!info.apps.length) {
|
|
35
35
|
const npxMissingWarning = (0, utils_1.commandExistsSync)("npx")
|
|
36
36
|
? ""
|
|
@@ -46,6 +46,8 @@ async function askQuestions(setup) {
|
|
|
46
46
|
{ name: `Flutter${flutterMissingWarning}`, value: "flutter" },
|
|
47
47
|
{ name: "skip", value: "skip" },
|
|
48
48
|
],
|
|
49
|
+
default: "skip",
|
|
50
|
+
nonInteractive: options?.nonInteractive,
|
|
49
51
|
});
|
|
50
52
|
try {
|
|
51
53
|
switch (choice) {
|
|
@@ -69,7 +71,7 @@ async function askQuestions(setup) {
|
|
|
69
71
|
setup.featureInfo = setup.featureInfo || {};
|
|
70
72
|
setup.featureInfo.dataconnectSdk = info;
|
|
71
73
|
}
|
|
72
|
-
async function chooseApp() {
|
|
74
|
+
async function chooseApp(options) {
|
|
73
75
|
let apps = dedupeAppsByPlatformAndDirectory(await (0, appUtils_1.detectApps)(cwd));
|
|
74
76
|
if (apps.length) {
|
|
75
77
|
(0, utils_1.logLabeledSuccess)("dataconnect", `Detected existing apps ${apps.map((a) => (0, appUtils_1.appDescription)(a)).join(", ")}`);
|
|
@@ -108,9 +110,12 @@ async function chooseApp() {
|
|
|
108
110
|
checked: a.directory === ".",
|
|
109
111
|
};
|
|
110
112
|
});
|
|
113
|
+
const defaultApps = choices.filter((c) => c.checked).map((c) => c.value);
|
|
111
114
|
const pickedApps = await (0, prompt_1.checkbox)({
|
|
112
115
|
message: "Which apps do you want to set up SQL Connect SDKs in?",
|
|
113
116
|
choices,
|
|
117
|
+
default: defaultApps.length > 0 ? defaultApps : [choices[0].value],
|
|
118
|
+
nonInteractive: options?.nonInteractive,
|
|
114
119
|
validate: (choices) => {
|
|
115
120
|
if (choices.length === 0) {
|
|
116
121
|
return "Please choose at least one app.";
|
|
@@ -39,7 +39,8 @@ async function initRules(setup, config, info) {
|
|
|
39
39
|
info.writeRules = await config.confirmWriteProjectFile(info.rulesFilename, info.rules);
|
|
40
40
|
}
|
|
41
41
|
async function getRulesFromConsole(projectId) {
|
|
42
|
-
const
|
|
42
|
+
const releases = await gcp.rules.listAllReleases(projectId);
|
|
43
|
+
const name = await gcp.rules.getLatestRulesetName(projectId, "cloud.firestore", releases);
|
|
43
44
|
if (!name) {
|
|
44
45
|
return null;
|
|
45
46
|
}
|
|
@@ -8,9 +8,11 @@ const templates_1 = require("../../../templates");
|
|
|
8
8
|
const PUBSPEC_TEMPLATE = (0, templates_1.readTemplateSync)("init/functions/dart/pubspec.yaml");
|
|
9
9
|
const MAIN_TEMPLATE = (0, templates_1.readTemplateSync)("init/functions/dart/server.dart");
|
|
10
10
|
const GITIGNORE_TEMPLATE = (0, templates_1.readTemplateSync)("init/functions/dart/_gitignore");
|
|
11
|
+
const ANALYSIS_OPTIONS_TEMPLATE = (0, templates_1.readTemplateSync)("init/functions/dart/analysis_options.yaml");
|
|
11
12
|
async function setup(setup, config) {
|
|
12
13
|
await config.askWriteProjectFile(`${setup.functions.source}/pubspec.yaml`, PUBSPEC_TEMPLATE);
|
|
13
14
|
await config.askWriteProjectFile(`${setup.functions.source}/.gitignore`, GITIGNORE_TEMPLATE);
|
|
15
|
+
await config.askWriteProjectFile(`${setup.functions.source}/analysis_options.yaml`, ANALYSIS_OPTIONS_TEMPLATE);
|
|
14
16
|
await config.askWriteProjectFile(`${setup.functions.source}/bin/server.dart`, MAIN_TEMPLATE);
|
|
15
17
|
config.set("functions.runtime", (0, supported_1.latest)("dart"));
|
|
16
18
|
config.set("functions.ignore", [".dart_tool", "build"]);
|
|
@@ -5,7 +5,8 @@ const gcp = require("../../../gcp");
|
|
|
5
5
|
const utils = require("../../../utils");
|
|
6
6
|
async function getRulesFromConsole(projectId) {
|
|
7
7
|
const defaultBucket = await gcp.storage.getDefaultBucket(projectId);
|
|
8
|
-
const
|
|
8
|
+
const releases = await gcp.rules.listAllReleases(projectId);
|
|
9
|
+
const name = await gcp.rules.getLatestRulesetName(projectId, "firebase.storage", releases, defaultBucket);
|
|
9
10
|
if (!name) {
|
|
10
11
|
return null;
|
|
11
12
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const ext_apps_1 = require("@modelcontextprotocol/ext-apps");
|
|
4
|
+
const app = new ext_apps_1.App({ name: "Update Firebase Environment", version: "1.0.0" });
|
|
5
|
+
const projectListContainer = document.getElementById("project-list");
|
|
6
|
+
const searchInput = document.getElementById("search-input");
|
|
7
|
+
const submitBtn = document.getElementById("submit-btn");
|
|
8
|
+
const statusBox = document.getElementById("status-box");
|
|
9
|
+
let projects = [];
|
|
10
|
+
let filteredProjects = [];
|
|
11
|
+
let selectedProjectId = null;
|
|
12
|
+
const envProjectIdEl = document.getElementById("env-project-id");
|
|
13
|
+
const envUserEl = document.getElementById("env-user");
|
|
14
|
+
function showStatus(message, type) {
|
|
15
|
+
statusBox.textContent = message;
|
|
16
|
+
statusBox.className = `status ${type}`;
|
|
17
|
+
statusBox.style.display = "block";
|
|
18
|
+
}
|
|
19
|
+
function renderProjects() {
|
|
20
|
+
projectListContainer.innerHTML = "";
|
|
21
|
+
if (filteredProjects.length === 0) {
|
|
22
|
+
projectListContainer.innerHTML = `
|
|
23
|
+
<div class="dropdown-item" style="cursor: default;">
|
|
24
|
+
<div class="item-name">No projects found.</div>
|
|
25
|
+
</div>
|
|
26
|
+
`;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
filteredProjects.forEach((p) => {
|
|
30
|
+
const item = document.createElement("div");
|
|
31
|
+
item.className = "dropdown-item";
|
|
32
|
+
if (p.projectId === selectedProjectId) {
|
|
33
|
+
item.classList.add("selected");
|
|
34
|
+
}
|
|
35
|
+
const displayName = p.displayName || p.projectId;
|
|
36
|
+
const projectId = p.projectId;
|
|
37
|
+
item.innerHTML = `
|
|
38
|
+
<div class="item-name">${displayName}</div>
|
|
39
|
+
<div class="item-id">${projectId}</div>
|
|
40
|
+
`;
|
|
41
|
+
item.onclick = () => {
|
|
42
|
+
selectedProjectId = projectId;
|
|
43
|
+
submitBtn.disabled = false;
|
|
44
|
+
renderProjects();
|
|
45
|
+
};
|
|
46
|
+
projectListContainer.appendChild(item);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
searchInput.oninput = () => {
|
|
50
|
+
const query = searchInput.value.toLowerCase().trim();
|
|
51
|
+
if (query === "") {
|
|
52
|
+
filteredProjects = projects;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
filteredProjects = projects.filter((p) => {
|
|
56
|
+
const name = (p.displayName || p.projectId).toLowerCase();
|
|
57
|
+
const id = p.projectId.toLowerCase();
|
|
58
|
+
return name.includes(query) || id.includes(query);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
renderProjects();
|
|
62
|
+
};
|
|
63
|
+
submitBtn.onclick = async () => {
|
|
64
|
+
if (!selectedProjectId)
|
|
65
|
+
return;
|
|
66
|
+
submitBtn.disabled = true;
|
|
67
|
+
showStatus(`Updating active project to ${selectedProjectId}...`, "info");
|
|
68
|
+
try {
|
|
69
|
+
const result = await app.callServerTool({
|
|
70
|
+
name: "firebase_update_environment",
|
|
71
|
+
arguments: { active_project: selectedProjectId },
|
|
72
|
+
});
|
|
73
|
+
const textContent = result.content?.find((c) => c.type === "text");
|
|
74
|
+
const text = textContent ? textContent.text : "Update complete.";
|
|
75
|
+
if (result.isError) {
|
|
76
|
+
showStatus(text, "error");
|
|
77
|
+
submitBtn.disabled = false;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
showStatus(text, "success");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
showStatus(`Error updating environment: ${err.message}`, "error");
|
|
85
|
+
submitBtn.disabled = false;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
app.onhostcontextchanged = (ctx) => {
|
|
89
|
+
if (ctx.theme)
|
|
90
|
+
(0, ext_apps_1.applyDocumentTheme)(ctx.theme);
|
|
91
|
+
if (ctx.styles?.variables)
|
|
92
|
+
(0, ext_apps_1.applyHostStyleVariables)(ctx.styles.variables);
|
|
93
|
+
if (ctx.styles?.css?.fonts)
|
|
94
|
+
(0, ext_apps_1.applyHostFonts)(ctx.styles.css.fonts);
|
|
95
|
+
if (ctx.safeAreaInsets) {
|
|
96
|
+
const { top, right, bottom, left } = ctx.safeAreaInsets;
|
|
97
|
+
document.body.style.padding = `${top}px ${right}px ${bottom}px ${left}px`;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
(async () => {
|
|
101
|
+
try {
|
|
102
|
+
await app.connect();
|
|
103
|
+
showStatus("Connecting to server...", "info");
|
|
104
|
+
try {
|
|
105
|
+
const envResult = await app.callServerTool({
|
|
106
|
+
name: "firebase_get_environment",
|
|
107
|
+
arguments: {},
|
|
108
|
+
});
|
|
109
|
+
const envData = envResult.structuredContent;
|
|
110
|
+
if (envData) {
|
|
111
|
+
envProjectIdEl.textContent = envData.projectId || "<NONE>";
|
|
112
|
+
envUserEl.textContent = envData.authenticatedUser || "<NONE>";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
console.error("Failed to fetch environment:", err);
|
|
117
|
+
showStatus(`Failed to fetch environment: ${err.message}`, "error");
|
|
118
|
+
}
|
|
119
|
+
const result = await app.callServerTool({ name: "firebase_list_projects", arguments: {} });
|
|
120
|
+
const data = result.structuredContent;
|
|
121
|
+
if (data && data.projects) {
|
|
122
|
+
projects = data.projects;
|
|
123
|
+
filteredProjects = projects;
|
|
124
|
+
renderProjects();
|
|
125
|
+
showStatus("Projects loaded successfully.", "success");
|
|
126
|
+
setTimeout(() => {
|
|
127
|
+
if (statusBox.className === "status success")
|
|
128
|
+
statusBox.style.display = "none";
|
|
129
|
+
}, 3000);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
showStatus("No projects returned from server.", "error");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
showStatus(`Failed to load projects: ${err.message}`, "error");
|
|
137
|
+
}
|
|
138
|
+
})();
|
package/lib/mcp/index.js
CHANGED
|
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.FirebaseMcpServer = void 0;
|
|
4
4
|
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
5
5
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
|
+
const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
7
|
+
const express_1 = require("express");
|
|
8
|
+
const cors_1 = require("cors");
|
|
6
9
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
7
10
|
const crossSpawn = require("cross-spawn");
|
|
8
11
|
const node_fs_1 = require("node:fs");
|
|
@@ -278,6 +281,9 @@ class FirebaseMcpServer {
|
|
|
278
281
|
}
|
|
279
282
|
const isBillingEnabled = projectId ? await this.safeCheckBillingEnabled(projectId) : false;
|
|
280
283
|
const toolsCtx = this._createMcpContext(projectId, accountEmail, isBillingEnabled);
|
|
284
|
+
if (request.params._meta?.progressToken) {
|
|
285
|
+
toolsCtx.progressToken = request.params._meta.progressToken;
|
|
286
|
+
}
|
|
281
287
|
try {
|
|
282
288
|
const res = await tool.fn(toolArgs, toolsCtx);
|
|
283
289
|
await this.trackGA4("mcp_tool_call", {
|
|
@@ -370,7 +376,56 @@ class FirebaseMcpServer {
|
|
|
370
376
|
}
|
|
371
377
|
return resolved.result;
|
|
372
378
|
}
|
|
373
|
-
async start() {
|
|
379
|
+
async start(options) {
|
|
380
|
+
if (options?.useSSE) {
|
|
381
|
+
const app = (0, express_1.default)();
|
|
382
|
+
app.use((0, cors_1.default)());
|
|
383
|
+
const port = options.port || 3000;
|
|
384
|
+
const transports = {};
|
|
385
|
+
app.get("/sse", async (req, res) => {
|
|
386
|
+
this.logger.debug(`[SSE] GET /sse connection attempt from ${req.ip}`);
|
|
387
|
+
try {
|
|
388
|
+
const transport = new sse_js_1.SSEServerTransport("/message", res);
|
|
389
|
+
const sessionId = transport.sessionId;
|
|
390
|
+
transports[sessionId] = transport;
|
|
391
|
+
this.logger.debug(`[SSE] Connected session ${sessionId}`);
|
|
392
|
+
await this.server.connect(transport);
|
|
393
|
+
this.logger.debug(`[SSE] Server connected to transport`);
|
|
394
|
+
await new Promise((resolve) => {
|
|
395
|
+
req.on("close", () => {
|
|
396
|
+
this.logger.debug(`[SSE] Session ${sessionId} disconnected`);
|
|
397
|
+
delete transports[sessionId];
|
|
398
|
+
resolve();
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
catch (err) {
|
|
403
|
+
this.logger.error(`[SSE] Connection error: ${err}`);
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
app.post("/message", async (req, res) => {
|
|
407
|
+
const sessionId = req.query.sessionId;
|
|
408
|
+
this.logger.debug(`[SSE] POST /message attempt for session ${sessionId}`);
|
|
409
|
+
const transport = transports[sessionId];
|
|
410
|
+
if (transport) {
|
|
411
|
+
try {
|
|
412
|
+
await transport.handlePostMessage(req, res);
|
|
413
|
+
this.logger.debug(`[SSE] Handled message for session ${sessionId}`);
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
this.logger.error(`[SSE] Error handling message for session ${sessionId}: ${err}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
this.logger.error(`[SSE] Rejecting message: No active transport found for session ${sessionId}`);
|
|
421
|
+
res.status(400).send("No active SSE transport connection found for this session");
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
app.listen(port, "127.0.0.1", () => {
|
|
425
|
+
this.logger.info(`MCP Server running on HTTP/SSE mode at http://127.0.0.1:${port}`);
|
|
426
|
+
});
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
374
429
|
const transport = process.env.FIREBASE_MCP_DEBUG_LOG
|
|
375
430
|
? new logging_transport_1.LoggingStdioServerTransport(process.env.FIREBASE_MCP_DEBUG_LOG)
|
|
376
431
|
: new stdio_js_1.StdioServerTransport();
|
|
@@ -384,7 +439,7 @@ class FirebaseMcpServer {
|
|
|
384
439
|
this.logger.debug("[mcp] Error on billingInfo for " +
|
|
385
440
|
projectId +
|
|
386
441
|
", failing open (assuming false): " +
|
|
387
|
-
(e.message
|
|
442
|
+
(e instanceof Error ? e.message : String(e)));
|
|
388
443
|
return false;
|
|
389
444
|
}
|
|
390
445
|
}
|
|
@@ -15,6 +15,7 @@ const crashlytics_investigations_1 = require("./guides/crashlytics_investigation
|
|
|
15
15
|
const track_1 = require("../../track");
|
|
16
16
|
const crashlytics_issues_1 = require("./guides/crashlytics_issues");
|
|
17
17
|
const crashlytics_reports_1 = require("./guides/crashlytics_reports");
|
|
18
|
+
const update_environment_ui_1 = require("./update_environment_ui");
|
|
18
19
|
exports.resources = [
|
|
19
20
|
app_id_1.app_id,
|
|
20
21
|
crashlytics_investigations_1.crashlytics_investigations,
|
|
@@ -26,6 +27,7 @@ exports.resources = [
|
|
|
26
27
|
init_firestore_rules_1.init_firestore_rules,
|
|
27
28
|
init_auth_1.init_auth,
|
|
28
29
|
init_hosting_1.init_hosting,
|
|
30
|
+
update_environment_ui_1.update_environment_ui,
|
|
29
31
|
];
|
|
30
32
|
exports.resourceTemplates = [docs_1.docs];
|
|
31
33
|
async function resolveResource(uri, ctx, track = true) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.update_environment_ui = void 0;
|
|
4
|
+
const resource_1 = require("../resource");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const fs = require("fs/promises");
|
|
7
|
+
const util_1 = require("../util");
|
|
8
|
+
const resourceUri = "ui://core/update_environment/mcp-app.html";
|
|
9
|
+
exports.update_environment_ui = (0, resource_1.resource)({
|
|
10
|
+
uri: resourceUri,
|
|
11
|
+
name: "Update Environment UI",
|
|
12
|
+
description: "Visual interface for selecting active Firebase project",
|
|
13
|
+
mimeType: util_1.RESOURCE_MIME_TYPE,
|
|
14
|
+
}, async () => {
|
|
15
|
+
try {
|
|
16
|
+
const htmlPath = path.join(__dirname, "../apps/update_environment/mcp-app.html");
|
|
17
|
+
const html = await fs.readFile(htmlPath, "utf-8");
|
|
18
|
+
return {
|
|
19
|
+
contents: [
|
|
20
|
+
{
|
|
21
|
+
uri: resourceUri,
|
|
22
|
+
mimeType: util_1.RESOURCE_MIME_TYPE,
|
|
23
|
+
text: html,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
30
|
+
throw new Error(`Failed to load Update Environment UI: ${message}`);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
@@ -46,7 +46,8 @@ exports.get_security_rules = (0, tool_1.tool)("core", {
|
|
|
46
46
|
storage: { productName: "Storage", releaseName: "firebase.storage" },
|
|
47
47
|
};
|
|
48
48
|
const { productName, releaseName } = serviceInfo[type];
|
|
49
|
-
const
|
|
49
|
+
const releases = await (0, rules_1.listAllReleases)(projectId);
|
|
50
|
+
const rulesetName = await (0, rules_1.getLatestRulesetName)(projectId, releaseName, releases);
|
|
50
51
|
if (!rulesetName)
|
|
51
52
|
return (0, util_1.mcpError)(`No active ${productName} rules were found in project '${projectId}'`);
|
|
52
53
|
const rules = await (0, rules_1.getRulesetContent)(rulesetName);
|
package/lib/mcp/util.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RESOURCE_MIME_TYPE = void 0;
|
|
3
4
|
exports.toContent = toContent;
|
|
5
|
+
exports.applyAppMeta = applyAppMeta;
|
|
4
6
|
exports.mcpError = mcpError;
|
|
5
7
|
exports.checkFeatureActive = checkFeatureActive;
|
|
6
8
|
exports.cleanSchema = cleanSchema;
|
|
7
9
|
const js_yaml_1 = require("js-yaml");
|
|
10
|
+
const experiments = require("../experiments");
|
|
8
11
|
const api_1 = require("../api");
|
|
9
12
|
const ensureApiEnabled_1 = require("../ensureApiEnabled");
|
|
10
13
|
const timeout_1 = require("../timeout");
|
|
@@ -25,8 +28,23 @@ function toContent(data, options) {
|
|
|
25
28
|
const suffix = options?.contentSuffix || "";
|
|
26
29
|
return {
|
|
27
30
|
content: [{ type: "text", text: `${prefix}${text}${suffix}` }],
|
|
31
|
+
structuredContent: data,
|
|
28
32
|
};
|
|
29
33
|
}
|
|
34
|
+
function applyAppMeta(result, resourceUri) {
|
|
35
|
+
if (experiments.isEnabled("mcpapps")) {
|
|
36
|
+
return {
|
|
37
|
+
...result,
|
|
38
|
+
_meta: {
|
|
39
|
+
...result._meta,
|
|
40
|
+
ui: {
|
|
41
|
+
resourceUri,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
30
48
|
function mcpError(message, code) {
|
|
31
49
|
let errorMessage = "unknown error";
|
|
32
50
|
if (message instanceof Error) {
|
|
@@ -223,3 +241,4 @@ function cleanSchema(schema) {
|
|
|
223
241
|
const result = deepClean(schema, true);
|
|
224
242
|
return result === null ? {} : result;
|
|
225
243
|
}
|
|
244
|
+
exports.RESOURCE_MIME_TYPE = "application/vnd.mcp.ext-app+html";
|
package/lib/rulesDeploy.js
CHANGED
|
@@ -30,10 +30,10 @@ class RulesDeploy {
|
|
|
30
30
|
this.options = options;
|
|
31
31
|
this.type = type;
|
|
32
32
|
this.project = options.project;
|
|
33
|
-
this.rulesFiles =
|
|
33
|
+
this.rulesFiles = [];
|
|
34
34
|
this.rulesetNames = {};
|
|
35
35
|
}
|
|
36
|
-
addFile(path) {
|
|
36
|
+
addFile(path, databaseId) {
|
|
37
37
|
const fullPath = this.options.config.path(path);
|
|
38
38
|
let src;
|
|
39
39
|
try {
|
|
@@ -43,15 +43,15 @@ class RulesDeploy {
|
|
|
43
43
|
logger_1.logger.debug("[rules read error]", e.stack);
|
|
44
44
|
throw new error_1.FirebaseError(`Error reading rules file ${(0, colorette_1.bold)(path)}`);
|
|
45
45
|
}
|
|
46
|
-
this.rulesFiles
|
|
46
|
+
this.rulesFiles.push({ path, files: [{ name: path, content: src }], databaseId });
|
|
47
47
|
}
|
|
48
48
|
async compile() {
|
|
49
|
-
await Promise.all(
|
|
50
|
-
return this.compileRuleset(
|
|
49
|
+
await Promise.all(this.rulesFiles.map((entry) => {
|
|
50
|
+
return this.compileRuleset(entry.path, entry.files);
|
|
51
51
|
}));
|
|
52
52
|
}
|
|
53
|
-
async getCurrentRules(service) {
|
|
54
|
-
const latestName = await gcp.rules.getLatestRulesetName(this.options.project, service);
|
|
53
|
+
async getCurrentRules(service, releases, databaseId) {
|
|
54
|
+
const latestName = await gcp.rules.getLatestRulesetName(this.options.project, service, releases, databaseId);
|
|
55
55
|
let latestContent = null;
|
|
56
56
|
if (latestName) {
|
|
57
57
|
latestContent = await gcp.rules.getRulesetContent(latestName);
|
|
@@ -88,24 +88,37 @@ class RulesDeploy {
|
|
|
88
88
|
}
|
|
89
89
|
async createRulesets(service) {
|
|
90
90
|
const createdRulesetNames = [];
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
for (const
|
|
91
|
+
const releases = await gcp.rules.listAllReleases(this.options.project);
|
|
92
|
+
const newRulesetsByKey = new Map();
|
|
93
|
+
for (const entry of this.rulesFiles) {
|
|
94
|
+
const { path: filename, files, databaseId } = entry;
|
|
95
|
+
const normalizedDatabaseId = databaseId === "(default)" ? undefined : databaseId;
|
|
96
|
+
const { latestName: latestRulesetName, latestContent: latestRulesetContent } = await this.getCurrentRules(service, releases, normalizedDatabaseId);
|
|
97
|
+
const key = this.type === RulesetServiceType.FIREBASE_STORAGE
|
|
98
|
+
? filename
|
|
99
|
+
: `${filename}:${databaseId || ""}`;
|
|
94
100
|
if (latestRulesetName && _.isEqual(files, latestRulesetContent)) {
|
|
95
101
|
utils.logLabeledBullet(RulesetType[this.type], `latest version of ${(0, colorette_1.bold)(filename)} already up to date, skipping upload...`);
|
|
96
|
-
this.rulesetNames[
|
|
102
|
+
this.rulesetNames[key] = latestRulesetName;
|
|
97
103
|
continue;
|
|
98
104
|
}
|
|
99
105
|
if (service === RulesetServiceType.FIREBASE_STORAGE) {
|
|
100
106
|
await this.checkStorageRulesIamPermissions(files[0]?.content);
|
|
101
107
|
}
|
|
102
108
|
utils.logLabeledBullet(RulesetType[this.type], `uploading rules ${(0, colorette_1.bold)(filename)}...`);
|
|
103
|
-
|
|
109
|
+
let attachmentPoint;
|
|
110
|
+
if (this.type === RulesetServiceType.CLOUD_FIRESTORE &&
|
|
111
|
+
databaseId &&
|
|
112
|
+
databaseId !== "(default)") {
|
|
113
|
+
const projectNumber = await (0, getProjectNumber_1.getProjectNumber)(this.options);
|
|
114
|
+
attachmentPoint = `firestore.googleapis.com/projects/${projectNumber}/databases/${databaseId}`;
|
|
115
|
+
}
|
|
116
|
+
newRulesetsByKey.set(key, gcp.rules.createRuleset(this.options.project, files, attachmentPoint));
|
|
104
117
|
}
|
|
105
118
|
try {
|
|
106
|
-
await Promise.all(
|
|
107
|
-
for (const [
|
|
108
|
-
this.rulesetNames[
|
|
119
|
+
await Promise.all(newRulesetsByKey.values());
|
|
120
|
+
for (const [key, rulesetName] of newRulesetsByKey) {
|
|
121
|
+
this.rulesetNames[key] = await rulesetName;
|
|
109
122
|
createdRulesetNames.push(await rulesetName);
|
|
110
123
|
}
|
|
111
124
|
}
|
|
@@ -141,7 +154,13 @@ class RulesDeploy {
|
|
|
141
154
|
if (resourceName === RulesetServiceType.FIREBASE_STORAGE && !subResourceName) {
|
|
142
155
|
throw new error_1.FirebaseError(`Cannot release resource type "${resourceName}"`);
|
|
143
156
|
}
|
|
144
|
-
|
|
157
|
+
const key = this.type === RulesetServiceType.FIREBASE_STORAGE
|
|
158
|
+
? filename
|
|
159
|
+
: `${filename}:${subResourceName || ""}`;
|
|
160
|
+
const releaseName = subResourceName && subResourceName !== "(default)"
|
|
161
|
+
? `${resourceName}/${subResourceName}`
|
|
162
|
+
: resourceName;
|
|
163
|
+
await gcp.rules.updateOrCreateRelease(this.options.project, this.rulesetNames[key], releaseName);
|
|
145
164
|
utils.logLabeledSuccess(RulesetType[this.type], `released rules ${(0, colorette_1.bold)(filename)} to ${(0, colorette_1.bold)(resourceName)}`);
|
|
146
165
|
}
|
|
147
166
|
async compileRuleset(filename, files) {
|