firebase-tools 15.16.0 → 15.17.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/api.js +1 -1
- package/lib/deploy/apphosting/release.js +1 -1
- package/lib/deploy/functions/prepare.js +113 -3
- package/lib/deploy/functions/services/ailogic.js +7 -0
- package/lib/deploy/functions/services/database.js +16 -0
- package/lib/deploy/functions/services/firestore.js +1 -0
- package/lib/deploy/functions/services/storage.js +15 -1
- package/lib/deploy/functions/triggerRegionHelper.js +111 -2
- package/lib/emulator/downloadableEmulatorInfo.json +23 -23
- package/lib/emulator/functionsEmulatorShared.js +2 -1
- package/lib/env.js +5 -1
- package/lib/firestore/api-sort.js +22 -0
- package/lib/firestore/api-types.js +11 -1
- package/lib/firestore/api.js +21 -1
- package/lib/firestore/fsConfig.js +8 -0
- package/lib/firestore/pretty-print.js +26 -8
- package/lib/frameworks/next/index.js +1 -1
- package/lib/mcp/apps/deploy/mcp-app.js +120 -0
- package/lib/mcp/apps/deploy/vite.config.js +16 -0
- package/lib/mcp/apps/init/mcp-app.js +230 -0
- package/lib/mcp/apps/init/vite.config.js +16 -0
- package/lib/mcp/apps/update_environment/mcp-app.js +38 -36
- package/lib/mcp/apps/update_environment/vite.config.js +16 -0
- package/lib/mcp/index.js +16 -5
- package/lib/mcp/resources/deploy_ui.js +31 -0
- package/lib/mcp/resources/index.js +4 -0
- package/lib/mcp/resources/init_ui.js +31 -0
- package/lib/mcp/resources/update_environment_ui.js +3 -3
- package/lib/mcp/tools/auth/get_users.js +1 -1
- package/lib/mcp/tools/core/deploy.js +87 -0
- package/lib/mcp/tools/core/deploy_status.js +32 -0
- package/lib/mcp/tools/core/index.js +4 -0
- package/lib/mcp/tools/core/init.js +3 -0
- package/lib/mcp/tools/core/update_environment.js +3 -0
- package/lib/mcp/tools/firestore/query_collection.js +1 -1
- package/lib/mcp/tools/functions/list_functions.js +2 -2
- package/lib/mcp/util/jobs.js +31 -0
- package/lib/mcp/util.js +5 -4
- package/lib/tsconfig.compile.tsbuildinfo +1 -1
- package/lib/tsconfig.publish.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/templates/init/functions/dart/pubspec.yaml +1 -1
- package/templates/init/functions/dart/server.dart +2 -2
|
@@ -19,6 +19,15 @@ class PrettyPrint {
|
|
|
19
19
|
logger_1.logger.info(this.prettyIndexString(index));
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
|
+
getDatabaseEdition(database) {
|
|
23
|
+
return !database.databaseEdition ||
|
|
24
|
+
database.databaseEdition === types.DatabaseEdition.DATABASE_EDITION_UNSPECIFIED
|
|
25
|
+
? types.DatabaseEdition.STANDARD
|
|
26
|
+
: database.databaseEdition;
|
|
27
|
+
}
|
|
28
|
+
getDatabaseApiType(database) {
|
|
29
|
+
return database.type;
|
|
30
|
+
}
|
|
22
31
|
prettyPrintDatabases(databases) {
|
|
23
32
|
if (databases.length === 0) {
|
|
24
33
|
logger_1.logger.info("No databases found.");
|
|
@@ -26,10 +35,18 @@ class PrettyPrint {
|
|
|
26
35
|
}
|
|
27
36
|
const sortedDatabases = databases.sort(sort.compareApiDatabase);
|
|
28
37
|
const table = new Table({
|
|
29
|
-
head: ["Database Name"],
|
|
30
|
-
colWidths: [
|
|
38
|
+
head: ["Database Name", "Edition", "Type"],
|
|
39
|
+
colWidths: [
|
|
40
|
+
Math.max(...sortedDatabases.map((database) => database.name.length + 5), 20),
|
|
41
|
+
20,
|
|
42
|
+
20,
|
|
43
|
+
],
|
|
31
44
|
});
|
|
32
|
-
table.push(...sortedDatabases.map((database) =>
|
|
45
|
+
table.push(...sortedDatabases.map((database) => {
|
|
46
|
+
const edition = this.getDatabaseEdition(database);
|
|
47
|
+
const apiType = this.getDatabaseApiType(database);
|
|
48
|
+
return [this.prettyDatabaseString(database), edition, apiType];
|
|
49
|
+
}));
|
|
33
50
|
logger_1.logger.info(table.toString());
|
|
34
51
|
}
|
|
35
52
|
prettyPrintDatabase(database) {
|
|
@@ -41,11 +58,9 @@ class PrettyPrint {
|
|
|
41
58
|
head: ["Field", "Value"],
|
|
42
59
|
colWidths: [30, colValueWidth],
|
|
43
60
|
});
|
|
44
|
-
const edition =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
: database.databaseEdition;
|
|
48
|
-
table.push(["Name", clc.yellow(database.name)], ["Create Time", clc.yellow(database.createTime)], ["Last Update Time", clc.yellow(database.updateTime)], ["Type", clc.yellow(database.type)], ["Edition", clc.yellow(edition)], ["Location", clc.yellow(database.locationId)], ["Delete Protection State", clc.yellow(database.deleteProtectionState)], ["Point In Time Recovery", clc.yellow(database.pointInTimeRecoveryEnablement)], ["Earliest Version Time", clc.yellow(database.earliestVersionTime)], ["Version Retention Period", clc.yellow(database.versionRetentionPeriod)]);
|
|
61
|
+
const edition = this.getDatabaseEdition(database);
|
|
62
|
+
const apiType = this.getDatabaseApiType(database);
|
|
63
|
+
table.push(["Name", clc.yellow(database.name)], ["Create Time", clc.yellow(database.createTime)], ["Last Update Time", clc.yellow(database.updateTime)], ["Type", clc.yellow(apiType)], ["Edition", clc.yellow(edition)], ["Location", clc.yellow(database.locationId)], ["Delete Protection State", clc.yellow(database.deleteProtectionState)], ["Point In Time Recovery", clc.yellow(database.pointInTimeRecoveryEnablement)], ["Earliest Version Time", clc.yellow(database.earliestVersionTime)], ["Version Retention Period", clc.yellow(database.versionRetentionPeriod)]);
|
|
49
64
|
if (database.cmekConfig) {
|
|
50
65
|
table.push(["KMS Key Name", clc.yellow(database.cmekConfig.kmsKeyName)]);
|
|
51
66
|
if (database.cmekConfig.activeKeyVersion) {
|
|
@@ -195,6 +210,9 @@ class PrettyPrint {
|
|
|
195
210
|
else if (field.arrayConfig) {
|
|
196
211
|
configString = field.arrayConfig;
|
|
197
212
|
}
|
|
213
|
+
else if (field.searchConfig) {
|
|
214
|
+
configString = "SEARCH";
|
|
215
|
+
}
|
|
198
216
|
else if (field.vectorConfig) {
|
|
199
217
|
configString = `VECTOR<${field.vectorConfig.dimension}>`;
|
|
200
218
|
}
|
|
@@ -31,7 +31,7 @@ const logger_1 = require("../../logger");
|
|
|
31
31
|
const env_1 = require("../../functions/env");
|
|
32
32
|
const DEFAULT_BUILD_SCRIPT = ["next build"];
|
|
33
33
|
const PUBLIC_DIR = "public";
|
|
34
|
-
exports.supportedRange = "12 -
|
|
34
|
+
exports.supportedRange = "12 - 16.0";
|
|
35
35
|
exports.name = "Next.js";
|
|
36
36
|
exports.support = "preview";
|
|
37
37
|
exports.type = 2;
|
|
@@ -0,0 +1,120 @@
|
|
|
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: "firebase-deploy", version: "1.0.0" });
|
|
5
|
+
const deployBtn = document.getElementById("deploy-btn");
|
|
6
|
+
const progressBar = document.getElementById("progress-bar");
|
|
7
|
+
const progressContainer = document.getElementById("progress-container");
|
|
8
|
+
const statusList = document.getElementById("status-list");
|
|
9
|
+
function addLog(message, type = "info") {
|
|
10
|
+
const item = document.createElement("div");
|
|
11
|
+
item.className = `status-item ${type}`;
|
|
12
|
+
item.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
|
|
13
|
+
statusList.appendChild(item);
|
|
14
|
+
statusList.scrollTop = statusList.scrollHeight;
|
|
15
|
+
}
|
|
16
|
+
function updateProgress(percentage) {
|
|
17
|
+
progressBar.value = percentage;
|
|
18
|
+
}
|
|
19
|
+
function pollStatus(jobId) {
|
|
20
|
+
let loggedCount = 0;
|
|
21
|
+
const interval = setInterval(async () => {
|
|
22
|
+
try {
|
|
23
|
+
const statusRes = await app.callServerTool({
|
|
24
|
+
name: "firebase_deploy_status",
|
|
25
|
+
arguments: { jobId },
|
|
26
|
+
});
|
|
27
|
+
if (statusRes.isError) {
|
|
28
|
+
addLog(`Failed to poll status: ${JSON.stringify(statusRes.content)}`, "error");
|
|
29
|
+
clearInterval(interval);
|
|
30
|
+
deployBtn.disabled = false;
|
|
31
|
+
deployBtn.textContent = "Deploy";
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const job = statusRes.structuredContent;
|
|
35
|
+
if (job) {
|
|
36
|
+
updateProgress(job.progress);
|
|
37
|
+
const newLogs = job.logs.slice(loggedCount);
|
|
38
|
+
newLogs.forEach((log) => addLog(log));
|
|
39
|
+
loggedCount = job.logs.length;
|
|
40
|
+
if (job.status === "success") {
|
|
41
|
+
addLog("Deployment completed successfully!", "success");
|
|
42
|
+
clearInterval(interval);
|
|
43
|
+
deployBtn.disabled = false;
|
|
44
|
+
deployBtn.textContent = "Deploy";
|
|
45
|
+
}
|
|
46
|
+
else if (job.status === "failed") {
|
|
47
|
+
addLog(`Deployment failed: ${job.error || "Unknown error"}`, "error");
|
|
48
|
+
clearInterval(interval);
|
|
49
|
+
deployBtn.disabled = false;
|
|
50
|
+
deployBtn.textContent = "Deploy";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
56
|
+
addLog(`Error during polling: ${message}`, "error");
|
|
57
|
+
clearInterval(interval);
|
|
58
|
+
deployBtn.disabled = false;
|
|
59
|
+
deployBtn.textContent = "Deploy";
|
|
60
|
+
}
|
|
61
|
+
}, 2000);
|
|
62
|
+
}
|
|
63
|
+
deployBtn.addEventListener("click", async () => {
|
|
64
|
+
const targets = [];
|
|
65
|
+
const checkboxes = document.querySelectorAll('.checkbox-grid input[type="checkbox"]:checked');
|
|
66
|
+
checkboxes.forEach((cb) => targets.push(cb.value));
|
|
67
|
+
if (targets.length === 0) {
|
|
68
|
+
addLog("Please select at least one service to deploy.", "error");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
deployBtn.disabled = true;
|
|
72
|
+
deployBtn.textContent = "Deploying...";
|
|
73
|
+
progressContainer.style.display = "block";
|
|
74
|
+
statusList.innerHTML = "";
|
|
75
|
+
updateProgress(10);
|
|
76
|
+
addLog(`Starting deployment for: ${targets.join(", ")}`);
|
|
77
|
+
try {
|
|
78
|
+
const onlyArg = targets.join(",");
|
|
79
|
+
addLog(`Calling firebase_deploy with only="${onlyArg}"...`);
|
|
80
|
+
const result = await app.callServerTool({
|
|
81
|
+
name: "firebase_deploy",
|
|
82
|
+
arguments: { only: onlyArg },
|
|
83
|
+
});
|
|
84
|
+
if (result.isError) {
|
|
85
|
+
addLog(`Deployment failed to start: ${JSON.stringify(result.content)}`, "error");
|
|
86
|
+
updateProgress(0);
|
|
87
|
+
deployBtn.disabled = false;
|
|
88
|
+
deployBtn.textContent = "Deploy";
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const jobId = result.structuredContent?.jobId;
|
|
92
|
+
if (jobId) {
|
|
93
|
+
addLog(`Deployment started with Job ID: ${jobId}. Polling status...`);
|
|
94
|
+
pollStatus(jobId);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
addLog("Failed to get Job ID from server.", "error");
|
|
98
|
+
deployBtn.disabled = false;
|
|
99
|
+
deployBtn.textContent = "Deploy";
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
105
|
+
addLog(`Error calling deploy tool: ${message}`, "error");
|
|
106
|
+
updateProgress(0);
|
|
107
|
+
deployBtn.disabled = false;
|
|
108
|
+
deployBtn.textContent = "Deploy";
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
void (async () => {
|
|
112
|
+
try {
|
|
113
|
+
await app.connect();
|
|
114
|
+
addLog("Connected to host.", "info");
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
118
|
+
console.error("Failed to connect app:", message);
|
|
119
|
+
}
|
|
120
|
+
})();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vite_1 = require("vite");
|
|
4
|
+
const vite_plugin_singlefile_1 = require("vite-plugin-singlefile");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
exports.default = (0, vite_1.defineConfig)({
|
|
7
|
+
plugins: [(0, vite_plugin_singlefile_1.viteSingleFile)()],
|
|
8
|
+
root: __dirname,
|
|
9
|
+
build: {
|
|
10
|
+
outDir: path.resolve(__dirname, "../../../../lib/mcp/apps/deploy"),
|
|
11
|
+
emptyOutDir: true,
|
|
12
|
+
rollupOptions: {
|
|
13
|
+
input: path.resolve(__dirname, "mcp-app.html"),
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -0,0 +1,230 @@
|
|
|
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: "firebase-init", version: "1.0.0" });
|
|
5
|
+
const initBtn = document.getElementById("init-btn");
|
|
6
|
+
const statusBox = document.getElementById("status-box");
|
|
7
|
+
const productRadios = document.getElementsByName("product");
|
|
8
|
+
const firestoreSection = document.getElementById("firestore-section");
|
|
9
|
+
const authSection = document.getElementById("auth-section");
|
|
10
|
+
const googleCheckbox = document.getElementById("auth-google");
|
|
11
|
+
const googleFields = document.getElementById("google-fields");
|
|
12
|
+
const searchInput = document.getElementById("search-input");
|
|
13
|
+
const projectListContainer = document.getElementById("project-list");
|
|
14
|
+
let projects = [];
|
|
15
|
+
let filteredProjects = [];
|
|
16
|
+
let selectedProjectId = null;
|
|
17
|
+
function setStatus(message, type = "info") {
|
|
18
|
+
statusBox.className = `status ${type}`;
|
|
19
|
+
statusBox.textContent = message;
|
|
20
|
+
statusBox.style.display = "block";
|
|
21
|
+
}
|
|
22
|
+
function renderProjects() {
|
|
23
|
+
projectListContainer.innerHTML = "";
|
|
24
|
+
if (filteredProjects.length === 0) {
|
|
25
|
+
const empty = document.createElement("div");
|
|
26
|
+
empty.className = "dropdown-item";
|
|
27
|
+
empty.style.cursor = "default";
|
|
28
|
+
empty.innerHTML = `<div class="item-name">No projects found</div>`;
|
|
29
|
+
projectListContainer.appendChild(empty);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
filteredProjects.forEach((project) => {
|
|
33
|
+
const item = document.createElement("div");
|
|
34
|
+
item.className = "dropdown-item";
|
|
35
|
+
if (project.projectId === selectedProjectId) {
|
|
36
|
+
item.classList.add("selected");
|
|
37
|
+
}
|
|
38
|
+
const displayName = project.displayName || project.projectId;
|
|
39
|
+
const projectId = project.projectId;
|
|
40
|
+
item.innerHTML = `
|
|
41
|
+
<div class="item-name">${displayName}</div>
|
|
42
|
+
<div class="item-id">${projectId}</div>
|
|
43
|
+
`;
|
|
44
|
+
item.onclick = () => {
|
|
45
|
+
selectedProjectId = projectId;
|
|
46
|
+
initBtn.disabled = false;
|
|
47
|
+
renderProjects();
|
|
48
|
+
};
|
|
49
|
+
projectListContainer.appendChild(item);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
searchInput.oninput = () => {
|
|
53
|
+
const query = searchInput.value.toLowerCase().trim();
|
|
54
|
+
if (query === "") {
|
|
55
|
+
filteredProjects = projects;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
filteredProjects = projects.filter((p) => {
|
|
59
|
+
const name = (p.displayName || p.projectId).toLowerCase();
|
|
60
|
+
const id = p.projectId.toLowerCase();
|
|
61
|
+
return name.includes(query) || id.includes(query);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
renderProjects();
|
|
65
|
+
};
|
|
66
|
+
productRadios.forEach((radio) => {
|
|
67
|
+
radio.addEventListener("change", (e) => {
|
|
68
|
+
const target = e.target;
|
|
69
|
+
if (target.checked) {
|
|
70
|
+
if (target.value === "firestore") {
|
|
71
|
+
firestoreSection.classList.add("active");
|
|
72
|
+
authSection.classList.remove("active");
|
|
73
|
+
}
|
|
74
|
+
else if (target.value === "auth") {
|
|
75
|
+
authSection.classList.add("active");
|
|
76
|
+
firestoreSection.classList.remove("active");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
googleCheckbox.addEventListener("change", (e) => {
|
|
82
|
+
const target = e.target;
|
|
83
|
+
if (target.checked) {
|
|
84
|
+
googleFields.classList.add("active");
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
googleFields.classList.remove("active");
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
initBtn.addEventListener("click", async () => {
|
|
91
|
+
const selectedProduct = Array.from(productRadios).find((r) => r.checked)?.value;
|
|
92
|
+
if (!selectedProjectId) {
|
|
93
|
+
setStatus("Please select a project first.", "error");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
initBtn.disabled = true;
|
|
97
|
+
initBtn.textContent = "Initializing...";
|
|
98
|
+
setStatus("Setting active project...", "info");
|
|
99
|
+
try {
|
|
100
|
+
const updateResult = await app.callServerTool({
|
|
101
|
+
name: "firebase_update_environment",
|
|
102
|
+
arguments: { active_project: selectedProjectId },
|
|
103
|
+
});
|
|
104
|
+
if (updateResult.isError) {
|
|
105
|
+
setStatus(`Failed to set active project: ${JSON.stringify(updateResult.content)}`, "error");
|
|
106
|
+
initBtn.disabled = false;
|
|
107
|
+
initBtn.textContent = "Initialize";
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
setStatus("Initializing product...", "info");
|
|
111
|
+
const args = { features: {} };
|
|
112
|
+
if (selectedProduct === "firestore") {
|
|
113
|
+
const dbId = document.getElementById("firestore-db-id").value;
|
|
114
|
+
const rulesFile = document.getElementById("firestore-rules-file").value;
|
|
115
|
+
args.features.firestore = {
|
|
116
|
+
database_id: dbId,
|
|
117
|
+
rules_filename: rulesFile,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
else if (selectedProduct === "auth") {
|
|
121
|
+
const emailEnabled = document.getElementById("auth-email").checked;
|
|
122
|
+
const anonymousEnabled = document.getElementById("auth-anonymous")
|
|
123
|
+
.checked;
|
|
124
|
+
const googleEnabled = googleCheckbox.checked;
|
|
125
|
+
args.features.auth = {
|
|
126
|
+
providers: {
|
|
127
|
+
emailPassword: emailEnabled,
|
|
128
|
+
anonymous: anonymousEnabled,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
if (googleEnabled) {
|
|
132
|
+
const displayName = document.getElementById("google-display-name")
|
|
133
|
+
.value;
|
|
134
|
+
const supportEmail = document.getElementById("google-support-email")
|
|
135
|
+
.value;
|
|
136
|
+
args.features.auth.providers.googleSignIn = {
|
|
137
|
+
oAuthBrandDisplayName: displayName,
|
|
138
|
+
supportEmail: supportEmail,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const res = await app.callServerTool({
|
|
143
|
+
name: "firebase_init",
|
|
144
|
+
arguments: args,
|
|
145
|
+
});
|
|
146
|
+
if (res.isError) {
|
|
147
|
+
setStatus(`Failed to initialize: ${JSON.stringify(res.content)}`, "error");
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
setStatus(`Successfully initialized ${selectedProduct}!`, "success");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
155
|
+
setStatus(`Error: ${message}`, "error");
|
|
156
|
+
}
|
|
157
|
+
finally {
|
|
158
|
+
initBtn.disabled = false;
|
|
159
|
+
initBtn.textContent = "Initialize";
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
app.onhostcontextchanged = (ctx) => {
|
|
163
|
+
if (ctx.theme)
|
|
164
|
+
(0, ext_apps_1.applyDocumentTheme)(ctx.theme);
|
|
165
|
+
if (ctx.styles?.variables)
|
|
166
|
+
(0, ext_apps_1.applyHostStyleVariables)(ctx.styles.variables);
|
|
167
|
+
if (ctx.styles?.css?.fonts)
|
|
168
|
+
(0, ext_apps_1.applyHostFonts)(ctx.styles.css.fonts);
|
|
169
|
+
if (ctx.safeAreaInsets) {
|
|
170
|
+
const { top, right, bottom, left } = ctx.safeAreaInsets;
|
|
171
|
+
document.body.style.padding = `${top}px ${right}px ${bottom}px ${left}px`;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
(async () => {
|
|
175
|
+
const envDirEl = document.getElementById("env-dir");
|
|
176
|
+
try {
|
|
177
|
+
await app.connect();
|
|
178
|
+
setStatus("Connecting to server...", "info");
|
|
179
|
+
try {
|
|
180
|
+
const envResult = await app.callServerTool({
|
|
181
|
+
name: "firebase_get_environment",
|
|
182
|
+
arguments: {},
|
|
183
|
+
});
|
|
184
|
+
if (envResult.isError) {
|
|
185
|
+
throw new Error(`Failed to fetch environment: ${JSON.stringify(envResult.content)}`);
|
|
186
|
+
}
|
|
187
|
+
const envData = envResult.structuredContent;
|
|
188
|
+
if (envData) {
|
|
189
|
+
envDirEl.textContent = envData.projectDir || "<NONE>";
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
console.error("Failed to fetch environment:", err);
|
|
194
|
+
envDirEl.textContent = "Error loading";
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const result = await app.callServerTool({
|
|
198
|
+
name: "firebase_list_projects",
|
|
199
|
+
arguments: { page_size: 1000 },
|
|
200
|
+
});
|
|
201
|
+
if (result.isError) {
|
|
202
|
+
throw new Error(`Failed to load projects: ${JSON.stringify(result.content)}`);
|
|
203
|
+
}
|
|
204
|
+
const data = result.structuredContent;
|
|
205
|
+
if (data && data.projects) {
|
|
206
|
+
projects = data.projects;
|
|
207
|
+
filteredProjects = projects;
|
|
208
|
+
renderProjects();
|
|
209
|
+
setStatus("Projects loaded.", "success");
|
|
210
|
+
setTimeout(() => {
|
|
211
|
+
if (statusBox.className === "status success")
|
|
212
|
+
statusBox.style.display = "none";
|
|
213
|
+
}, 2000);
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
setStatus("No projects returned from server.", "error");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
221
|
+
setStatus(`Failed to load projects: ${message}`, "error");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
226
|
+
setStatus(`Failed to connect: ${message}`, "error");
|
|
227
|
+
if (envDirEl)
|
|
228
|
+
envDirEl.textContent = "Error loading";
|
|
229
|
+
}
|
|
230
|
+
})();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vite_1 = require("vite");
|
|
4
|
+
const vite_plugin_singlefile_1 = require("vite-plugin-singlefile");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
exports.default = (0, vite_1.defineConfig)({
|
|
7
|
+
plugins: [(0, vite_plugin_singlefile_1.viteSingleFile)()],
|
|
8
|
+
root: __dirname,
|
|
9
|
+
build: {
|
|
10
|
+
outDir: path_1.default.resolve(__dirname, "../../../../lib/mcp/apps/init"),
|
|
11
|
+
emptyOutDir: true,
|
|
12
|
+
rollupOptions: {
|
|
13
|
+
input: path_1.default.resolve(__dirname, "mcp-app.html"),
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
|
@@ -19,46 +19,43 @@ function showStatus(message, type) {
|
|
|
19
19
|
function renderProjects() {
|
|
20
20
|
projectListContainer.innerHTML = "";
|
|
21
21
|
if (filteredProjects.length === 0) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
`;
|
|
22
|
+
const opt = document.createElement("option");
|
|
23
|
+
opt.disabled = true;
|
|
24
|
+
opt.textContent = "No projects found.";
|
|
25
|
+
projectListContainer.appendChild(opt);
|
|
27
26
|
return;
|
|
28
27
|
}
|
|
29
28
|
filteredProjects.forEach((p) => {
|
|
30
|
-
const
|
|
31
|
-
|
|
29
|
+
const opt = document.createElement("option");
|
|
30
|
+
opt.value = p.projectId;
|
|
31
|
+
opt.textContent = p.displayName ? `${p.displayName} (${p.projectId})` : p.projectId;
|
|
32
32
|
if (p.projectId === selectedProjectId) {
|
|
33
|
-
|
|
33
|
+
opt.selected = true;
|
|
34
34
|
}
|
|
35
|
-
|
|
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);
|
|
35
|
+
projectListContainer.appendChild(opt);
|
|
47
36
|
});
|
|
48
37
|
}
|
|
38
|
+
projectListContainer.onchange = () => {
|
|
39
|
+
selectedProjectId = projectListContainer.value;
|
|
40
|
+
submitBtn.disabled = false;
|
|
41
|
+
};
|
|
42
|
+
let searchTimeout;
|
|
49
43
|
searchInput.oninput = () => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
44
|
+
clearTimeout(searchTimeout);
|
|
45
|
+
searchTimeout = setTimeout(() => {
|
|
46
|
+
const query = searchInput.value.toLowerCase().trim();
|
|
47
|
+
if (query === "") {
|
|
48
|
+
filteredProjects = projects;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
filteredProjects = projects.filter((p) => {
|
|
52
|
+
const name = (p.displayName || p.projectId).toLowerCase();
|
|
53
|
+
const id = p.projectId.toLowerCase();
|
|
54
|
+
return name.includes(query) || id.includes(query);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
renderProjects();
|
|
58
|
+
}, 300);
|
|
62
59
|
};
|
|
63
60
|
submitBtn.onclick = async () => {
|
|
64
61
|
if (!selectedProjectId)
|
|
@@ -81,10 +78,13 @@ submitBtn.onclick = async () => {
|
|
|
81
78
|
}
|
|
82
79
|
}
|
|
83
80
|
catch (err) {
|
|
84
|
-
|
|
81
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
82
|
+
showStatus(`Error updating environment: ${msg}`, "error");
|
|
85
83
|
submitBtn.disabled = false;
|
|
86
84
|
}
|
|
87
85
|
};
|
|
86
|
+
app.ontoolresult = (_result) => {
|
|
87
|
+
};
|
|
88
88
|
app.onhostcontextchanged = (ctx) => {
|
|
89
89
|
if (ctx.theme)
|
|
90
90
|
(0, ext_apps_1.applyDocumentTheme)(ctx.theme);
|
|
@@ -113,8 +113,9 @@ app.onhostcontextchanged = (ctx) => {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
catch (err) {
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
117
|
+
console.error("Failed to fetch environment:", msg);
|
|
118
|
+
showStatus(`Failed to fetch environment: ${msg}`, "error");
|
|
118
119
|
}
|
|
119
120
|
const result = await app.callServerTool({ name: "firebase_list_projects", arguments: {} });
|
|
120
121
|
const data = result.structuredContent;
|
|
@@ -133,6 +134,7 @@ app.onhostcontextchanged = (ctx) => {
|
|
|
133
134
|
}
|
|
134
135
|
}
|
|
135
136
|
catch (err) {
|
|
136
|
-
|
|
137
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
138
|
+
showStatus(`Failed to load projects: ${msg}`, "error");
|
|
137
139
|
}
|
|
138
140
|
})();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vite_1 = require("vite");
|
|
4
|
+
const vite_plugin_singlefile_1 = require("vite-plugin-singlefile");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
exports.default = (0, vite_1.defineConfig)({
|
|
7
|
+
plugins: [(0, vite_plugin_singlefile_1.viteSingleFile)()],
|
|
8
|
+
root: __dirname,
|
|
9
|
+
build: {
|
|
10
|
+
outDir: path_1.default.resolve(__dirname, "../../../../lib/mcp/apps/update_environment"),
|
|
11
|
+
emptyOutDir: true,
|
|
12
|
+
rollupOptions: {
|
|
13
|
+
input: path_1.default.resolve(__dirname, "mcp-app.html"),
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|
package/lib/mcp/index.js
CHANGED
|
@@ -4,11 +4,12 @@ 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
6
|
const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
7
|
-
const
|
|
8
|
-
const
|
|
7
|
+
const express = require("express");
|
|
8
|
+
const cors = require("cors");
|
|
9
9
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
10
10
|
const crossSpawn = require("cross-spawn");
|
|
11
11
|
const node_fs_1 = require("node:fs");
|
|
12
|
+
const experiments = require("../experiments");
|
|
12
13
|
const command_1 = require("../command");
|
|
13
14
|
const config_1 = require("../config");
|
|
14
15
|
const configstore_1 = require("../configstore");
|
|
@@ -246,8 +247,18 @@ class FirebaseMcpServer {
|
|
|
246
247
|
const skipAutoAuthForStudio = (0, env_1.isFirebaseStudio)();
|
|
247
248
|
this.logger.debug(`skip auto-auth in studio environment: ${skipAutoAuthForStudio}`);
|
|
248
249
|
const availableTools = await this.getAvailableTools();
|
|
250
|
+
const isMcpAppsEnabled = experiments.isEnabled("mcpapps");
|
|
249
251
|
return {
|
|
250
|
-
tools: availableTools.map((t) =>
|
|
252
|
+
tools: availableTools.map((t) => {
|
|
253
|
+
if (isMcpAppsEnabled)
|
|
254
|
+
return t.mcp;
|
|
255
|
+
if (t.mcp._meta?.ui) {
|
|
256
|
+
const restMeta = { ...t.mcp._meta };
|
|
257
|
+
delete restMeta.ui;
|
|
258
|
+
return { ...t.mcp, _meta: restMeta };
|
|
259
|
+
}
|
|
260
|
+
return t.mcp;
|
|
261
|
+
}),
|
|
251
262
|
_meta: {
|
|
252
263
|
projectRoot: this.cachedProjectDir,
|
|
253
264
|
projectDetected: hasActiveProject,
|
|
@@ -378,8 +389,8 @@ class FirebaseMcpServer {
|
|
|
378
389
|
}
|
|
379
390
|
async start(options) {
|
|
380
391
|
if (options?.useSSE) {
|
|
381
|
-
const app = (
|
|
382
|
-
app.use((
|
|
392
|
+
const app = express();
|
|
393
|
+
app.use(cors());
|
|
383
394
|
const port = options.port || 3000;
|
|
384
395
|
const transports = {};
|
|
385
396
|
app.get("/sse", async (req, res) => {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deploy_ui = exports.RESOURCE_MIME_TYPE = void 0;
|
|
4
|
+
const resource_1 = require("../resource");
|
|
5
|
+
const path = require("path");
|
|
6
|
+
const fs = require("fs/promises");
|
|
7
|
+
exports.RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
8
|
+
const resourceUri = "ui://core/deploy/mcp-app.html";
|
|
9
|
+
exports.deploy_ui = (0, resource_1.resource)({
|
|
10
|
+
uri: resourceUri,
|
|
11
|
+
name: "Deploy UI",
|
|
12
|
+
description: "Visual interface for Firebase Deploy",
|
|
13
|
+
mimeType: exports.RESOURCE_MIME_TYPE,
|
|
14
|
+
}, async () => {
|
|
15
|
+
try {
|
|
16
|
+
const htmlPath = path.join(__dirname, "../apps/deploy/mcp-app.html");
|
|
17
|
+
const html = await fs.readFile(htmlPath, "utf-8");
|
|
18
|
+
return {
|
|
19
|
+
contents: [
|
|
20
|
+
{
|
|
21
|
+
uri: resourceUri,
|
|
22
|
+
mimeType: exports.RESOURCE_MIME_TYPE,
|
|
23
|
+
text: html,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
throw new Error(`Failed to load Deploy UI: ${e.message}`);
|
|
30
|
+
}
|
|
31
|
+
});
|