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.
Files changed (43) hide show
  1. package/lib/api.js +1 -1
  2. package/lib/deploy/apphosting/release.js +1 -1
  3. package/lib/deploy/functions/prepare.js +113 -3
  4. package/lib/deploy/functions/services/ailogic.js +7 -0
  5. package/lib/deploy/functions/services/database.js +16 -0
  6. package/lib/deploy/functions/services/firestore.js +1 -0
  7. package/lib/deploy/functions/services/storage.js +15 -1
  8. package/lib/deploy/functions/triggerRegionHelper.js +111 -2
  9. package/lib/emulator/downloadableEmulatorInfo.json +23 -23
  10. package/lib/emulator/functionsEmulatorShared.js +2 -1
  11. package/lib/env.js +5 -1
  12. package/lib/firestore/api-sort.js +22 -0
  13. package/lib/firestore/api-types.js +11 -1
  14. package/lib/firestore/api.js +21 -1
  15. package/lib/firestore/fsConfig.js +8 -0
  16. package/lib/firestore/pretty-print.js +26 -8
  17. package/lib/frameworks/next/index.js +1 -1
  18. package/lib/mcp/apps/deploy/mcp-app.js +120 -0
  19. package/lib/mcp/apps/deploy/vite.config.js +16 -0
  20. package/lib/mcp/apps/init/mcp-app.js +230 -0
  21. package/lib/mcp/apps/init/vite.config.js +16 -0
  22. package/lib/mcp/apps/update_environment/mcp-app.js +38 -36
  23. package/lib/mcp/apps/update_environment/vite.config.js +16 -0
  24. package/lib/mcp/index.js +16 -5
  25. package/lib/mcp/resources/deploy_ui.js +31 -0
  26. package/lib/mcp/resources/index.js +4 -0
  27. package/lib/mcp/resources/init_ui.js +31 -0
  28. package/lib/mcp/resources/update_environment_ui.js +3 -3
  29. package/lib/mcp/tools/auth/get_users.js +1 -1
  30. package/lib/mcp/tools/core/deploy.js +87 -0
  31. package/lib/mcp/tools/core/deploy_status.js +32 -0
  32. package/lib/mcp/tools/core/index.js +4 -0
  33. package/lib/mcp/tools/core/init.js +3 -0
  34. package/lib/mcp/tools/core/update_environment.js +3 -0
  35. package/lib/mcp/tools/firestore/query_collection.js +1 -1
  36. package/lib/mcp/tools/functions/list_functions.js +2 -2
  37. package/lib/mcp/util/jobs.js +31 -0
  38. package/lib/mcp/util.js +5 -4
  39. package/lib/tsconfig.compile.tsbuildinfo +1 -1
  40. package/lib/tsconfig.publish.tsbuildinfo +1 -1
  41. package/package.json +1 -1
  42. package/templates/init/functions/dart/pubspec.yaml +1 -1
  43. 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: [Math.max(...sortedDatabases.map((database) => database.name.length + 5), 20)],
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) => [this.prettyDatabaseString(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 = !database.databaseEdition ||
45
- database.databaseEdition === types.DatabaseEdition.DATABASE_EDITION_UNSPECIFIED
46
- ? types.DatabaseEdition.STANDARD
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 - 15.0";
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
- projectListContainer.innerHTML = `
23
- <div class="dropdown-item" style="cursor: default;">
24
- <div class="item-name">No projects found.</div>
25
- </div>
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 item = document.createElement("div");
31
- item.className = "dropdown-item";
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
- item.classList.add("selected");
33
+ opt.selected = true;
34
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);
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
- 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();
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
- showStatus(`Error updating environment: ${err.message}`, "error");
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
- console.error("Failed to fetch environment:", err);
117
- showStatus(`Failed to fetch environment: ${err.message}`, "error");
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
- showStatus(`Failed to load projects: ${err.message}`, "error");
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 express_1 = require("express");
8
- const cors_1 = require("cors");
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) => t.mcp),
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 = (0, express_1.default)();
382
- app.use((0, cors_1.default)());
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
+ });