Skip to content

Commit 5c96996

Browse files
committed
feat: add Deploy MCP App
Adds the Deploy MCP App UI and resources. Also includes a small fix for firestore deploy to avoid errors on empty indexes. - Verified build and file changes.
1 parent bbb70ff commit 5c96996

File tree

5 files changed

+431
-0
lines changed

5 files changed

+431
-0
lines changed

src/deploy/firestore/deploy.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ async function deployIndexes(context: any, options: any): Promise<void> {
3030
return;
3131
}
3232
const indexesContext: IndexContext[] = context?.firestore?.indexes;
33+
if (!indexesContext || indexesContext.length === 0) {
34+
return;
35+
}
3336

3437
utils.logBullet(clc.bold(clc.cyan("firestore: ")) + "deploying indexes...");
3538
const firestoreIndexes = new FirestoreApi();

src/mcp/apps/deploy/mcp-app.html

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Firebase Deploy</title>
7+
<style>
8+
:root {
9+
--color-bg: #0f172a;
10+
--color-surface: rgba(30, 41, 59, 0.7);
11+
--color-primary: #38bdf8;
12+
--color-primary-hover: #7dd3fc;
13+
--color-text: #f8fafc;
14+
--color-text-secondary: #94a3b8;
15+
--color-border: rgba(255, 255, 255, 0.1);
16+
--border-radius-md: 12px;
17+
--border-radius-lg: 16px;
18+
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
19+
}
20+
21+
body {
22+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
23+
background-color: var(--color-bg);
24+
color: var(--color-text);
25+
margin: 0;
26+
padding: 24px;
27+
display: flex;
28+
justify-content: center;
29+
align-items: center;
30+
min-height: 100vh;
31+
}
32+
33+
.container {
34+
background: var(--color-surface);
35+
backdrop-filter: blur(12px);
36+
-webkit-backdrop-filter: blur(12px);
37+
border: 1px solid var(--color-border);
38+
border-radius: var(--border-radius-lg);
39+
padding: 32px;
40+
width: 100%;
41+
max-width: 500px;
42+
box-shadow: var(--shadow);
43+
text-align: center;
44+
}
45+
46+
.header {
47+
margin-bottom: 32px;
48+
}
49+
50+
h1 {
51+
margin: 0;
52+
font-size: 24px;
53+
font-weight: 700;
54+
background: linear-gradient(135deg, #fff 0%, #cbd5e1 100%);
55+
-webkit-background-clip: text;
56+
-webkit-text-fill-color: transparent;
57+
}
58+
59+
.subtitle {
60+
color: var(--color-text-secondary);
61+
font-size: 14px;
62+
margin-top: 8px;
63+
}
64+
65+
.targets-section {
66+
text-align: left;
67+
margin-bottom: 24px;
68+
}
69+
70+
.targets-section h3 {
71+
font-size: 14px;
72+
color: var(--color-text-secondary);
73+
margin-bottom: 12px;
74+
}
75+
76+
.checkbox-grid {
77+
display: grid;
78+
grid-template-columns: repeat(2, 1fr);
79+
gap: 12px;
80+
}
81+
82+
.checkbox-item {
83+
display: flex;
84+
align-items: center;
85+
gap: 8px;
86+
padding: 12px;
87+
background: rgba(255, 255, 255, 0.05);
88+
border: 1px solid var(--color-border);
89+
border-radius: var(--border-radius-md);
90+
cursor: pointer;
91+
transition: all 0.2s ease;
92+
}
93+
94+
.checkbox-item:hover {
95+
background: rgba(255, 255, 255, 0.1);
96+
border-color: var(--color-primary);
97+
}
98+
99+
.checkbox-item input {
100+
cursor: pointer;
101+
}
102+
103+
.checkbox-item label {
104+
cursor: pointer;
105+
font-size: 14px;
106+
flex: 1;
107+
}
108+
109+
.actions {
110+
margin-top: 32px;
111+
}
112+
113+
button {
114+
width: 100%;
115+
background: var(--color-primary);
116+
color: #0f172a;
117+
border: none;
118+
padding: 12px;
119+
border-radius: var(--border-radius-md);
120+
font-size: 16px;
121+
font-weight: 600;
122+
cursor: pointer;
123+
transition: all 0.2s ease;
124+
box-shadow: 0 4px 12px rgba(56, 189, 248, 0.3);
125+
}
126+
127+
button:hover {
128+
background: var(--color-primary-hover);
129+
transform: translateY(-1px);
130+
box-shadow: 0 6px 16px rgba(56, 189, 248, 0.4);
131+
}
132+
133+
button:active {
134+
transform: translateY(0);
135+
}
136+
137+
button:disabled {
138+
background: #475569;
139+
color: #94a3b8;
140+
cursor: not-allowed;
141+
box-shadow: none;
142+
transform: none;
143+
}
144+
145+
.progress-section {
146+
margin-top: 32px;
147+
text-align: left;
148+
display: none; /* Shown during deploy */
149+
}
150+
151+
.progress-section h3 {
152+
font-size: 14px;
153+
color: var(--color-text-secondary);
154+
margin-bottom: 12px;
155+
}
156+
157+
.progress-bar-container {
158+
height: 8px;
159+
background: rgba(255, 255, 255, 0.1);
160+
border-radius: 4px;
161+
overflow: hidden;
162+
margin-bottom: 12px;
163+
}
164+
165+
.progress-bar {
166+
height: 100%;
167+
background: var(--color-primary);
168+
width: 0%;
169+
transition: width 0.3s ease;
170+
}
171+
172+
.status-list {
173+
display: flex;
174+
flex-direction: column;
175+
gap: 8px;
176+
max-height: 200px;
177+
overflow-y: auto;
178+
font-size: 13px;
179+
color: var(--color-text-secondary);
180+
}
181+
182+
.status-item {
183+
padding: 8px;
184+
background: rgba(255, 255, 255, 0.02);
185+
border-radius: 6px;
186+
}
187+
188+
.status-item.success {
189+
color: #10b981;
190+
}
191+
192+
.status-item.error {
193+
color: #ef4444;
194+
}
195+
</style>
196+
</head>
197+
<body>
198+
<div class="container">
199+
<div class="header">
200+
<h1>Deploy Firebase</h1>
201+
<p class="subtitle">Select services and trigger deployment.</p>
202+
</div>
203+
204+
<div class="targets-section">
205+
<h3>Services to Deploy</h3>
206+
<div class="checkbox-grid">
207+
<div class="checkbox-item">
208+
<input type="checkbox" id="target-hosting" value="hosting" checked />
209+
<label for="target-hosting">Hosting</label>
210+
</div>
211+
<div class="checkbox-item">
212+
<input type="checkbox" id="target-functions" value="functions" />
213+
<label for="target-functions">Functions</label>
214+
</div>
215+
<div class="checkbox-item">
216+
<input type="checkbox" id="target-firestore" value="firestore" />
217+
<label for="target-firestore">Firestore</label>
218+
</div>
219+
<div class="checkbox-item">
220+
<input type="checkbox" id="target-storage" value="storage" />
221+
<label for="target-storage">Storage</label>
222+
</div>
223+
<div class="checkbox-item">
224+
<input type="checkbox" id="target-database" value="database" />
225+
<label for="target-database">Database</label>
226+
</div>
227+
<div class="checkbox-item">
228+
<input type="checkbox" id="target-rules" value="remoteconfig" />
229+
<label for="target-rules">Remote Config</label>
230+
</div>
231+
<div class="checkbox-item">
232+
<input type="checkbox" id="target-extensions" value="extensions" />
233+
<label for="target-extensions">Extensions</label>
234+
</div>
235+
<div class="checkbox-item">
236+
<input type="checkbox" id="target-dataconnect" value="dataconnect" />
237+
<label for="target-dataconnect">Data Connect</label>
238+
</div>
239+
</div>
240+
</div>
241+
242+
<div class="actions">
243+
<button id="deploy-btn">Deploy</button>
244+
</div>
245+
246+
<div id="progress-container" class="progress-section">
247+
<h3>Deployment Progress</h3>
248+
<div class="progress-bar-container">
249+
<div id="progress-bar" class="progress-bar"></div>
250+
</div>
251+
<div id="status-list" class="status-list">
252+
<!-- Logs go here -->
253+
</div>
254+
</div>
255+
</div>
256+
<script type="module" src="mcp-app.ts"></script>
257+
</body>
258+
</html>

src/mcp/apps/deploy/mcp-app.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { App } from "@modelcontextprotocol/ext-apps";
2+
3+
const app = new App({ name: "firebase-deploy", version: "1.0.0" });
4+
5+
const deployBtn = document.getElementById("deploy-btn") as HTMLButtonElement;
6+
const progressBar = document.getElementById("progress-bar") as HTMLDivElement;
7+
const progressContainer = document.getElementById("progress-container") as HTMLDivElement;
8+
const statusList = document.getElementById("status-list") as HTMLDivElement;
9+
10+
function addLog(message: string, type: "info" | "success" | "error" = "info") {
11+
const item = document.createElement("div");
12+
item.className = `status-item ${type}`;
13+
item.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
14+
statusList.appendChild(item);
15+
statusList.scrollTop = statusList.scrollHeight; // Auto-scroll
16+
}
17+
18+
function updateProgress(percentage: number) {
19+
progressBar.style.width = `${percentage}%`;
20+
}
21+
22+
function pollStatus(jobId: string) {
23+
const interval = setInterval(async () => {
24+
try {
25+
const statusRes = await app.callServerTool({
26+
name: "firebase_deploy_status",
27+
arguments: { jobId },
28+
});
29+
30+
if (statusRes.isError) {
31+
addLog(`Failed to poll status: ${JSON.stringify(statusRes.content)}`, "error");
32+
clearInterval(interval);
33+
deployBtn.disabled = false;
34+
deployBtn.textContent = "Deploy";
35+
return;
36+
}
37+
38+
const job = statusRes.structuredContent as any;
39+
if (job) {
40+
updateProgress(job.progress);
41+
42+
// Clear and redraw logs to avoid duplication if we are reading full history
43+
statusList.innerHTML = "";
44+
job.logs.forEach((log: string) => addLog(log));
45+
46+
if (job.status === "success") {
47+
addLog("Deployment completed successfully!", "success");
48+
clearInterval(interval);
49+
deployBtn.disabled = false;
50+
deployBtn.textContent = "Deploy";
51+
} else if (job.status === "failed") {
52+
addLog(`Deployment failed: ${job.error}`, "error");
53+
clearInterval(interval);
54+
deployBtn.disabled = false;
55+
deployBtn.textContent = "Deploy";
56+
}
57+
}
58+
} catch (err: any) {
59+
addLog(`Error during polling: ${err.message}`, "error");
60+
clearInterval(interval);
61+
deployBtn.disabled = false;
62+
deployBtn.textContent = "Deploy";
63+
}
64+
}, 2000);
65+
}
66+
67+
deployBtn.addEventListener("click", async () => {
68+
// 1. Get checked targets
69+
const targets: string[] = [];
70+
const checkboxes = document.querySelectorAll(
71+
'.checkbox-grid input[type="checkbox"]:checked',
72+
) as NodeListOf<HTMLInputElement>;
73+
checkboxes.forEach((cb) => targets.push(cb.value));
74+
75+
if (targets.length === 0) {
76+
addLog("Please select at least one service to deploy.", "error");
77+
return;
78+
}
79+
80+
// 2. Disable UI
81+
deployBtn.disabled = true;
82+
deployBtn.textContent = "Deploying...";
83+
progressContainer.style.display = "block";
84+
statusList.innerHTML = ""; // Clear old logs
85+
updateProgress(10);
86+
addLog(`Starting deployment for: ${targets.join(", ")}`);
87+
88+
// 3. Call tool
89+
try {
90+
const onlyArg = targets.join(",");
91+
addLog(`Calling firebase_deploy with only="${onlyArg}"...`);
92+
93+
const result = await app.callServerTool({
94+
name: "firebase_deploy",
95+
arguments: { only: onlyArg },
96+
});
97+
98+
if (result.isError) {
99+
addLog(`Deployment failed to start: ${JSON.stringify(result.content)}`, "error");
100+
updateProgress(0);
101+
deployBtn.disabled = false;
102+
deployBtn.textContent = "Deploy";
103+
} else {
104+
const jobId = (result.structuredContent as any)?.jobId;
105+
if (jobId) {
106+
addLog(`Deployment started with Job ID: ${jobId}. Polling status...`);
107+
pollStatus(jobId);
108+
} else {
109+
addLog("Failed to get Job ID from server.", "error");
110+
deployBtn.disabled = false;
111+
deployBtn.textContent = "Deploy";
112+
}
113+
}
114+
} catch (err: any) {
115+
addLog(`Error calling deploy tool: ${err.message}`, "error");
116+
updateProgress(0);
117+
deployBtn.disabled = false;
118+
deployBtn.textContent = "Deploy";
119+
}
120+
});
121+
122+
(async () => {
123+
try {
124+
await app.connect();
125+
addLog("Connected to host.", "info");
126+
} catch (err: any) {
127+
console.error("Failed to connect app:", err);
128+
}
129+
})();

0 commit comments

Comments
 (0)