@hostingguru/mcp-server 1.0.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/README.md +94 -0
- package/main.cjs +1449 -0
- package/package.json +42 -0
package/main.cjs
ADDED
|
@@ -0,0 +1,1449 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __copyProps = (to, from, except, desc) => {
|
|
8
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
9
|
+
for (let key of __getOwnPropNames(from))
|
|
10
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
11
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
12
|
+
}
|
|
13
|
+
return to;
|
|
14
|
+
};
|
|
15
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
16
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
17
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
18
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
19
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
20
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
21
|
+
mod
|
|
22
|
+
));
|
|
23
|
+
|
|
24
|
+
// apps/mcp-server/src/main.ts
|
|
25
|
+
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
26
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
27
|
+
|
|
28
|
+
// apps/mcp-server/src/config.ts
|
|
29
|
+
var path = __toESM(require("path"));
|
|
30
|
+
var os = __toESM(require("os"));
|
|
31
|
+
var CONFIG = {
|
|
32
|
+
version: "1.0.0",
|
|
33
|
+
defaultApiUrl: "https://backend.hostingguru.io/hostingguru",
|
|
34
|
+
configDir: path.join(os.homedir(), ".hostingguru"),
|
|
35
|
+
credentialsFile: path.join(os.homedir(), ".hostingguru", "credentials.json")
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// apps/mcp-server/src/state/credentials.ts
|
|
39
|
+
var fs = __toESM(require("fs"));
|
|
40
|
+
|
|
41
|
+
// apps/mcp-server/src/state/session.ts
|
|
42
|
+
var Session = class {
|
|
43
|
+
constructor() {
|
|
44
|
+
this.token = null;
|
|
45
|
+
this.workspaceId = null;
|
|
46
|
+
this.apiUrl = CONFIG.defaultApiUrl;
|
|
47
|
+
}
|
|
48
|
+
setAuth(token) {
|
|
49
|
+
this.token = token;
|
|
50
|
+
}
|
|
51
|
+
setWorkspace(workspaceId) {
|
|
52
|
+
this.workspaceId = workspaceId;
|
|
53
|
+
}
|
|
54
|
+
clear() {
|
|
55
|
+
this.token = null;
|
|
56
|
+
this.workspaceId = null;
|
|
57
|
+
}
|
|
58
|
+
isAuthenticated() {
|
|
59
|
+
return this.token !== null;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var session = new Session();
|
|
63
|
+
|
|
64
|
+
// apps/mcp-server/src/state/crypto.ts
|
|
65
|
+
var crypto = __toESM(require("crypto"));
|
|
66
|
+
var os2 = __toESM(require("os"));
|
|
67
|
+
var ALGORITHM = "aes-256-gcm";
|
|
68
|
+
var IV_LENGTH = 16;
|
|
69
|
+
var TAG_LENGTH = 16;
|
|
70
|
+
function deriveKey() {
|
|
71
|
+
const material = [
|
|
72
|
+
os2.hostname(),
|
|
73
|
+
os2.userInfo().username,
|
|
74
|
+
os2.homedir(),
|
|
75
|
+
"hostingguru-mcp-v1"
|
|
76
|
+
].join(":");
|
|
77
|
+
return crypto.createHash("sha256").update(material).digest();
|
|
78
|
+
}
|
|
79
|
+
function encrypt(plaintext) {
|
|
80
|
+
const key = deriveKey();
|
|
81
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
82
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
83
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
84
|
+
const tag = cipher.getAuthTag();
|
|
85
|
+
return Buffer.concat([iv, tag, encrypted]).toString("base64");
|
|
86
|
+
}
|
|
87
|
+
function decrypt(encoded) {
|
|
88
|
+
const key = deriveKey();
|
|
89
|
+
const data = Buffer.from(encoded, "base64");
|
|
90
|
+
const iv = data.subarray(0, IV_LENGTH);
|
|
91
|
+
const tag = data.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
|
|
92
|
+
const ciphertext = data.subarray(IV_LENGTH + TAG_LENGTH);
|
|
93
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
94
|
+
decipher.setAuthTag(tag);
|
|
95
|
+
return decipher.update(ciphertext) + decipher.final("utf8");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// apps/mcp-server/src/state/credentials.ts
|
|
99
|
+
function loadCredentials() {
|
|
100
|
+
try {
|
|
101
|
+
if (!fs.existsSync(CONFIG.credentialsFile))
|
|
102
|
+
return;
|
|
103
|
+
const raw = fs.readFileSync(CONFIG.credentialsFile, "utf-8");
|
|
104
|
+
const stored = JSON.parse(raw);
|
|
105
|
+
let payload;
|
|
106
|
+
if (stored.v === 2 && stored.encrypted) {
|
|
107
|
+
payload = JSON.parse(decrypt(stored.encrypted));
|
|
108
|
+
} else if (stored.token) {
|
|
109
|
+
payload = stored;
|
|
110
|
+
} else {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (payload.token) {
|
|
114
|
+
session.setAuth(payload.token);
|
|
115
|
+
}
|
|
116
|
+
if (payload.selectedWorkspaceId) {
|
|
117
|
+
session.setWorkspace(payload.selectedWorkspaceId);
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function saveCredentials() {
|
|
123
|
+
try {
|
|
124
|
+
if (!fs.existsSync(CONFIG.configDir)) {
|
|
125
|
+
fs.mkdirSync(CONFIG.configDir, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
const payload = {
|
|
128
|
+
token: session.token,
|
|
129
|
+
selectedWorkspaceId: session.workspaceId || void 0
|
|
130
|
+
};
|
|
131
|
+
const stored = {
|
|
132
|
+
encrypted: encrypt(JSON.stringify(payload)),
|
|
133
|
+
v: 2
|
|
134
|
+
};
|
|
135
|
+
fs.writeFileSync(CONFIG.credentialsFile, JSON.stringify(stored, null, 2), { mode: 384 });
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function clearCredentials() {
|
|
140
|
+
try {
|
|
141
|
+
if (fs.existsSync(CONFIG.credentialsFile)) {
|
|
142
|
+
fs.unlinkSync(CONFIG.credentialsFile);
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
session.clear();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// apps/mcp-server/src/tools/auth.ts
|
|
150
|
+
var import_zod = require("zod");
|
|
151
|
+
|
|
152
|
+
// apps/mcp-server/src/api/client.ts
|
|
153
|
+
var import_axios = __toESM(require("axios"));
|
|
154
|
+
var _client = null;
|
|
155
|
+
function getClient() {
|
|
156
|
+
if (_client && _client.defaults.baseURL === session.apiUrl) {
|
|
157
|
+
return _client;
|
|
158
|
+
}
|
|
159
|
+
_client = import_axios.default.create({
|
|
160
|
+
baseURL: session.apiUrl,
|
|
161
|
+
timeout: 3e4,
|
|
162
|
+
headers: { "Content-Type": "application/json" }
|
|
163
|
+
});
|
|
164
|
+
_client.interceptors.request.use((config) => {
|
|
165
|
+
if (session.token) {
|
|
166
|
+
config.headers.Cookie = `token=${session.token}`;
|
|
167
|
+
}
|
|
168
|
+
if (session.workspaceId) {
|
|
169
|
+
config.headers["X-Workspace-Id"] = session.workspaceId;
|
|
170
|
+
}
|
|
171
|
+
return config;
|
|
172
|
+
});
|
|
173
|
+
_client.interceptors.response.use(
|
|
174
|
+
(response) => response,
|
|
175
|
+
(error) => {
|
|
176
|
+
if (error.response?.status === 401) {
|
|
177
|
+
clearCredentials();
|
|
178
|
+
}
|
|
179
|
+
return Promise.reject(error);
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
return _client;
|
|
183
|
+
}
|
|
184
|
+
function resetClient() {
|
|
185
|
+
_client = null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// apps/mcp-server/src/api/workspaces.ts
|
|
189
|
+
async function listWorkspaces() {
|
|
190
|
+
const res = await getClient().get("/workspaces");
|
|
191
|
+
return res.data;
|
|
192
|
+
}
|
|
193
|
+
async function createWorkspace(name, slug) {
|
|
194
|
+
const res = await getClient().post("/workspaces", { name, slug });
|
|
195
|
+
return res.data;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// apps/mcp-server/src/api/auth.ts
|
|
199
|
+
var import_axios2 = __toESM(require("axios"));
|
|
200
|
+
async function emailAuth(apiUrl, email, password) {
|
|
201
|
+
const res = await import_axios2.default.post(
|
|
202
|
+
`${apiUrl}/auth/email`,
|
|
203
|
+
{ email, password },
|
|
204
|
+
{
|
|
205
|
+
timeout: 3e4,
|
|
206
|
+
headers: { "Content-Type": "application/json" },
|
|
207
|
+
validateStatus: () => true
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
if (res.status >= 400) {
|
|
211
|
+
const err = new Error(res.data?.message || "Authentication failed");
|
|
212
|
+
err.code = res.data?.error;
|
|
213
|
+
err.status = res.status;
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
const token = extractTokenCookie(res.headers["set-cookie"]);
|
|
217
|
+
if (!token) {
|
|
218
|
+
throw new Error("Backend did not return an auth cookie. Check API_URL.");
|
|
219
|
+
}
|
|
220
|
+
return { data: res.data, token };
|
|
221
|
+
}
|
|
222
|
+
async function logout() {
|
|
223
|
+
try {
|
|
224
|
+
await getClient().post("/users/logout");
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function extractTokenCookie(setCookie) {
|
|
229
|
+
if (!setCookie)
|
|
230
|
+
return null;
|
|
231
|
+
const cookies = Array.isArray(setCookie) ? setCookie : [setCookie];
|
|
232
|
+
for (const raw of cookies) {
|
|
233
|
+
const first = raw.split(";")[0].trim();
|
|
234
|
+
const eq = first.indexOf("=");
|
|
235
|
+
if (eq < 0)
|
|
236
|
+
continue;
|
|
237
|
+
if (first.slice(0, eq) === "token") {
|
|
238
|
+
return decodeURIComponent(first.slice(eq + 1));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// apps/mcp-server/src/auth/browser-auth.ts
|
|
245
|
+
var http = __toESM(require("http"));
|
|
246
|
+
var crypto2 = __toESM(require("crypto"));
|
|
247
|
+
|
|
248
|
+
// apps/mcp-server/src/auth/login-page.ts
|
|
249
|
+
var DASHBOARD_URL = "https://dashboard.hostingguru.io";
|
|
250
|
+
var DISCORD_URL = "https://discord.gg/bPkuBrKMaG";
|
|
251
|
+
var TELEGRAM_URL = "https://t.me/hgsupportadmin";
|
|
252
|
+
var WHATSAPP_URL = "https://chat.whatsapp.com/C9ulBmvm2FYJ68hoaVo5rp?mode=gi_t";
|
|
253
|
+
function getLoginPageHtml(callbackPort, nonce) {
|
|
254
|
+
return `<!DOCTYPE html>
|
|
255
|
+
<html lang="en">
|
|
256
|
+
<head>
|
|
257
|
+
<meta charset="UTF-8">
|
|
258
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
259
|
+
<title>Sign in to HostingGuru</title>
|
|
260
|
+
<style>
|
|
261
|
+
:root {
|
|
262
|
+
--bg: #0a0a0a;
|
|
263
|
+
--fg: rgba(255,255,255,0.9);
|
|
264
|
+
--muted: rgba(255,255,255,0.55);
|
|
265
|
+
--dim: rgba(255,255,255,0.35);
|
|
266
|
+
--border: rgba(255,255,255,0.08);
|
|
267
|
+
--border-strong: rgba(255,255,255,0.12);
|
|
268
|
+
--surface: rgba(255,255,255,0.025);
|
|
269
|
+
--aurora-cyan: #7dd3fc;
|
|
270
|
+
--aurora-violet: #c4b5fd;
|
|
271
|
+
--aurora-pink: #f9a8d4;
|
|
272
|
+
--aurora-pink-deep: #f472b6;
|
|
273
|
+
--success: #34d399;
|
|
274
|
+
}
|
|
275
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
276
|
+
body {
|
|
277
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', sans-serif;
|
|
278
|
+
background: #050505;
|
|
279
|
+
color: var(--fg);
|
|
280
|
+
display: flex;
|
|
281
|
+
align-items: center;
|
|
282
|
+
justify-content: center;
|
|
283
|
+
min-height: 100vh;
|
|
284
|
+
padding: 2rem 1rem;
|
|
285
|
+
}
|
|
286
|
+
.container { width: 100%; max-width: 420px; }
|
|
287
|
+
.logo { display: flex; justify-content: center; margin-bottom: 1.5rem; }
|
|
288
|
+
.logo img { height: 2rem; }
|
|
289
|
+
.hero { text-align: center; margin-bottom: 1.5rem; }
|
|
290
|
+
.hero h1 {
|
|
291
|
+
font-size: 26px;
|
|
292
|
+
font-weight: 500;
|
|
293
|
+
line-height: 1.15;
|
|
294
|
+
letter-spacing: -0.02em;
|
|
295
|
+
color: #fff;
|
|
296
|
+
}
|
|
297
|
+
.hero h1 .aurora {
|
|
298
|
+
background: linear-gradient(90deg, var(--aurora-cyan), var(--aurora-violet), var(--aurora-pink));
|
|
299
|
+
-webkit-background-clip: text;
|
|
300
|
+
background-clip: text;
|
|
301
|
+
color: transparent;
|
|
302
|
+
}
|
|
303
|
+
.hero p { font-size: 14px; color: var(--muted); margin-top: 0.5rem; line-height: 1.55; }
|
|
304
|
+
.card {
|
|
305
|
+
border: 1px solid var(--border);
|
|
306
|
+
background: var(--surface);
|
|
307
|
+
border-radius: 0.75rem;
|
|
308
|
+
padding: 1.5rem;
|
|
309
|
+
}
|
|
310
|
+
.oauth-note {
|
|
311
|
+
font-size: 12.5px;
|
|
312
|
+
color: var(--dim);
|
|
313
|
+
text-align: center;
|
|
314
|
+
padding: 0.75rem;
|
|
315
|
+
border: 1px dashed var(--border);
|
|
316
|
+
border-radius: 0.5rem;
|
|
317
|
+
margin-bottom: 0.875rem;
|
|
318
|
+
line-height: 1.55;
|
|
319
|
+
}
|
|
320
|
+
.oauth-note a { color: var(--aurora-cyan); text-decoration: none; }
|
|
321
|
+
.oauth-note a:hover { color: var(--aurora-violet); }
|
|
322
|
+
.field { margin-bottom: 0.875rem; }
|
|
323
|
+
.field-header {
|
|
324
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
325
|
+
margin-bottom: 0.375rem;
|
|
326
|
+
}
|
|
327
|
+
label { font-size: 12.5px; color: rgba(255,255,255,0.6); }
|
|
328
|
+
.forgot-link {
|
|
329
|
+
font-size: 12px;
|
|
330
|
+
color: var(--dim);
|
|
331
|
+
text-decoration: none;
|
|
332
|
+
transition: color 0.15s;
|
|
333
|
+
}
|
|
334
|
+
.forgot-link:hover { color: var(--aurora-cyan); }
|
|
335
|
+
input {
|
|
336
|
+
display: block;
|
|
337
|
+
height: 2.75rem;
|
|
338
|
+
width: 100%;
|
|
339
|
+
border-radius: 0.5rem;
|
|
340
|
+
border: 1px solid var(--border-strong);
|
|
341
|
+
background: rgba(255,255,255,0.04);
|
|
342
|
+
padding: 0.5rem 0.75rem;
|
|
343
|
+
font-size: 14px;
|
|
344
|
+
color: var(--fg);
|
|
345
|
+
outline: none;
|
|
346
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
347
|
+
}
|
|
348
|
+
input::placeholder { color: rgba(255,255,255,0.25); }
|
|
349
|
+
input:focus {
|
|
350
|
+
border-color: rgba(255,255,255,0.2);
|
|
351
|
+
box-shadow: 0 0 0 1px rgba(255,255,255,0.2);
|
|
352
|
+
}
|
|
353
|
+
.hint {
|
|
354
|
+
font-size: 11.5px;
|
|
355
|
+
color: var(--dim);
|
|
356
|
+
margin-top: 0.375rem;
|
|
357
|
+
line-height: 1.5;
|
|
358
|
+
}
|
|
359
|
+
button.submit {
|
|
360
|
+
display: inline-flex;
|
|
361
|
+
align-items: center;
|
|
362
|
+
justify-content: center;
|
|
363
|
+
gap: 0.4rem;
|
|
364
|
+
width: 100%;
|
|
365
|
+
height: 2.75rem;
|
|
366
|
+
border: none;
|
|
367
|
+
border-radius: 0.5rem;
|
|
368
|
+
background: #fff;
|
|
369
|
+
color: #0a0a0a;
|
|
370
|
+
font-size: 14.5px;
|
|
371
|
+
font-weight: 500;
|
|
372
|
+
cursor: pointer;
|
|
373
|
+
transition: background 0.15s, opacity 0.15s;
|
|
374
|
+
}
|
|
375
|
+
button.submit:hover { background: rgba(255,255,255,0.92); }
|
|
376
|
+
button.submit:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
377
|
+
.arrow { display: inline-block; width: 14px; height: 14px; }
|
|
378
|
+
.error {
|
|
379
|
+
border: 1px solid rgba(249,168,212,0.24);
|
|
380
|
+
background: rgba(249,168,212,0.08);
|
|
381
|
+
color: var(--aurora-pink);
|
|
382
|
+
border-radius: 0.5rem;
|
|
383
|
+
padding: 0.75rem;
|
|
384
|
+
font-size: 13px;
|
|
385
|
+
margin-bottom: 0.875rem;
|
|
386
|
+
line-height: 1.45;
|
|
387
|
+
display: none;
|
|
388
|
+
}
|
|
389
|
+
.error a { color: var(--aurora-pink-deep); text-decoration: underline; text-underline-offset: 2px; }
|
|
390
|
+
.terms {
|
|
391
|
+
font-size: 12px;
|
|
392
|
+
color: rgba(255,255,255,0.45);
|
|
393
|
+
text-align: center;
|
|
394
|
+
margin-top: 0.75rem;
|
|
395
|
+
line-height: 1.55;
|
|
396
|
+
}
|
|
397
|
+
.terms a { color: rgba(255,255,255,0.7); text-decoration: underline; text-underline-offset: 2px; }
|
|
398
|
+
.terms a:hover { color: #fff; }
|
|
399
|
+
.trust {
|
|
400
|
+
display: flex; flex-wrap: wrap; justify-content: center;
|
|
401
|
+
gap: 0.5rem 1rem;
|
|
402
|
+
font-size: 12px; color: var(--dim);
|
|
403
|
+
margin-top: 1.25rem;
|
|
404
|
+
}
|
|
405
|
+
.trust span { display: inline-flex; align-items: center; gap: 0.4rem; }
|
|
406
|
+
.dot {
|
|
407
|
+
width: 6px; height: 6px; border-radius: 999px;
|
|
408
|
+
background: var(--success); box-shadow: 0 0 0 2px rgba(52,211,153,0.2);
|
|
409
|
+
}
|
|
410
|
+
.footer {
|
|
411
|
+
text-align: center;
|
|
412
|
+
font-size: 12.5px;
|
|
413
|
+
color: rgba(255,255,255,0.45);
|
|
414
|
+
margin-top: 1.25rem;
|
|
415
|
+
padding: 0.875rem 1rem;
|
|
416
|
+
border: 1px dashed var(--border);
|
|
417
|
+
border-radius: 0.5rem;
|
|
418
|
+
line-height: 1.55;
|
|
419
|
+
}
|
|
420
|
+
.footer a {
|
|
421
|
+
color: var(--aurora-cyan);
|
|
422
|
+
font-weight: 500;
|
|
423
|
+
text-decoration: none;
|
|
424
|
+
transition: color 0.15s;
|
|
425
|
+
}
|
|
426
|
+
.footer a:hover { color: var(--aurora-violet); }
|
|
427
|
+
.success-wrap { text-align: center; padding: 2rem 1rem; }
|
|
428
|
+
.success-wrap h2 {
|
|
429
|
+
font-size: 18px; font-weight: 500; color: #fff;
|
|
430
|
+
margin-bottom: 0.5rem;
|
|
431
|
+
}
|
|
432
|
+
.success-wrap p { font-size: 14px; color: var(--muted); line-height: 1.55; }
|
|
433
|
+
.success-icon {
|
|
434
|
+
width: 48px; height: 48px;
|
|
435
|
+
border-radius: 999px;
|
|
436
|
+
background: rgba(52,211,153,0.12);
|
|
437
|
+
color: var(--success);
|
|
438
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
439
|
+
margin-bottom: 1rem;
|
|
440
|
+
}
|
|
441
|
+
</style>
|
|
442
|
+
</head>
|
|
443
|
+
<body>
|
|
444
|
+
<div class="container">
|
|
445
|
+
<div id="login-view">
|
|
446
|
+
<div class="logo">
|
|
447
|
+
<svg width="160" height="32" viewBox="0 0 160 32" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="HostingGuru">
|
|
448
|
+
<text x="0" y="22" font-family="-apple-system, 'Inter', sans-serif" font-size="19" font-weight="600" fill="white">HostingGuru</text>
|
|
449
|
+
</svg>
|
|
450
|
+
</div>
|
|
451
|
+
|
|
452
|
+
<div class="hero">
|
|
453
|
+
<h1>Ship your first app<br>in <span class="aurora">two minutes</span>.</h1>
|
|
454
|
+
<p>Sign in or create your account \xB7 Free forever, no card.</p>
|
|
455
|
+
</div>
|
|
456
|
+
|
|
457
|
+
<form class="card" onsubmit="event.preventDefault(); handleLogin();">
|
|
458
|
+
<div class="oauth-note">
|
|
459
|
+
Using Google or GitHub?
|
|
460
|
+
Sign in at <a href="${DASHBOARD_URL}/auth" target="_blank" rel="noopener">dashboard.hostingguru.io</a>,
|
|
461
|
+
copy a session token, and run <code>auth_token</code> in your AI tool.
|
|
462
|
+
</div>
|
|
463
|
+
|
|
464
|
+
<div id="error" class="error"></div>
|
|
465
|
+
|
|
466
|
+
<div class="field">
|
|
467
|
+
<div class="field-header">
|
|
468
|
+
<label for="email">Email</label>
|
|
469
|
+
</div>
|
|
470
|
+
<input id="email" type="email" placeholder="you@yourstartup.com" autocomplete="email" autofocus required />
|
|
471
|
+
</div>
|
|
472
|
+
|
|
473
|
+
<div class="field">
|
|
474
|
+
<div class="field-header">
|
|
475
|
+
<label for="password">Password</label>
|
|
476
|
+
<a class="forgot-link" href="${DASHBOARD_URL}/forgot-password" target="_blank" rel="noopener">Forgot password?</a>
|
|
477
|
+
</div>
|
|
478
|
+
<input id="password" type="password" placeholder="At least 8 characters" autocomplete="current-password" minlength="8" required />
|
|
479
|
+
<p class="hint">If your email is new, we'll create your account. Otherwise we'll sign you in.</p>
|
|
480
|
+
</div>
|
|
481
|
+
|
|
482
|
+
<button id="submit" type="submit" class="submit">
|
|
483
|
+
<span id="submit-label">Continue</span>
|
|
484
|
+
<svg class="arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
|
485
|
+
</button>
|
|
486
|
+
|
|
487
|
+
<p class="terms">
|
|
488
|
+
By continuing, you agree to our
|
|
489
|
+
<a href="https://hostingguru.io/terms" target="_blank" rel="noopener">Terms</a>
|
|
490
|
+
and
|
|
491
|
+
<a href="https://hostingguru.io/privacy" target="_blank" rel="noopener">Privacy Policy</a>.
|
|
492
|
+
</p>
|
|
493
|
+
</form>
|
|
494
|
+
|
|
495
|
+
<div class="trust">
|
|
496
|
+
<span><span class="dot"></span>Free forever</span>
|
|
497
|
+
<span><span class="dot"></span>No card required</span>
|
|
498
|
+
<span><span class="dot"></span>EU & US regions</span>
|
|
499
|
+
</div>
|
|
500
|
+
|
|
501
|
+
<p class="footer">
|
|
502
|
+
Your credentials are sent to the HostingGuru API over HTTPS \u2014 they never pass through your AI tool.<br>
|
|
503
|
+
Need a hand?
|
|
504
|
+
<a href="${DISCORD_URL}" target="_blank" rel="noopener">Discord</a> \xB7
|
|
505
|
+
<a href="${TELEGRAM_URL}" target="_blank" rel="noopener">Telegram</a> \xB7
|
|
506
|
+
<a href="${WHATSAPP_URL}" target="_blank" rel="noopener">WhatsApp</a>
|
|
507
|
+
</p>
|
|
508
|
+
</div>
|
|
509
|
+
|
|
510
|
+
<div id="success-view" class="card success-wrap" style="display:none">
|
|
511
|
+
<div class="success-icon" aria-hidden="true">
|
|
512
|
+
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>
|
|
513
|
+
</div>
|
|
514
|
+
<h2 id="success-title">You're signed in</h2>
|
|
515
|
+
<p id="success-message">You can close this tab and return to your AI tool.</p>
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
|
|
519
|
+
<script>
|
|
520
|
+
const CALLBACK = 'http://localhost:${callbackPort}/submit';
|
|
521
|
+
const NONCE = '${nonce}';
|
|
522
|
+
|
|
523
|
+
async function handleLogin() {
|
|
524
|
+
const email = document.getElementById('email').value.trim();
|
|
525
|
+
const password = document.getElementById('password').value;
|
|
526
|
+
const errorEl = document.getElementById('error');
|
|
527
|
+
const submitBtn = document.getElementById('submit');
|
|
528
|
+
const submitLabel = document.getElementById('submit-label');
|
|
529
|
+
errorEl.style.display = 'none';
|
|
530
|
+
|
|
531
|
+
if (!email || !password) {
|
|
532
|
+
showError('Email and password are required.');
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
submitBtn.disabled = true;
|
|
537
|
+
submitLabel.textContent = 'Signing in\u2026';
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
const res = await fetch(CALLBACK, {
|
|
541
|
+
method: 'POST',
|
|
542
|
+
headers: { 'Content-Type': 'application/json' },
|
|
543
|
+
body: JSON.stringify({ email, password, nonce: NONCE }),
|
|
544
|
+
});
|
|
545
|
+
const data = await res.json();
|
|
546
|
+
|
|
547
|
+
if (!res.ok) {
|
|
548
|
+
showError(data.message || 'Sign-in failed.', data.error);
|
|
549
|
+
submitBtn.disabled = false;
|
|
550
|
+
submitLabel.textContent = 'Continue';
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
document.getElementById('login-view').style.display = 'none';
|
|
555
|
+
document.getElementById('success-view').style.display = 'block';
|
|
556
|
+
if (data.new) {
|
|
557
|
+
document.getElementById('success-title').textContent = 'Account created';
|
|
558
|
+
document.getElementById('success-message').textContent =
|
|
559
|
+
'Check your inbox to verify your email, then return to your AI tool.';
|
|
560
|
+
}
|
|
561
|
+
} catch (err) {
|
|
562
|
+
showError('Could not reach the local sign-in helper. Is the AI tool still running?');
|
|
563
|
+
submitBtn.disabled = false;
|
|
564
|
+
submitLabel.textContent = 'Continue';
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function showError(message, code) {
|
|
569
|
+
const errorEl = document.getElementById('error');
|
|
570
|
+
if (code === 'INVALID_CREDENTIALS') {
|
|
571
|
+
errorEl.innerHTML = message + ' <a href="${DASHBOARD_URL}/forgot-password" target="_blank" rel="noopener">Reset it</a> or try again.';
|
|
572
|
+
} else {
|
|
573
|
+
errorEl.textContent = message;
|
|
574
|
+
}
|
|
575
|
+
errorEl.style.display = 'block';
|
|
576
|
+
}
|
|
577
|
+
</script>
|
|
578
|
+
</body>
|
|
579
|
+
</html>`;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// apps/mcp-server/src/auth/browser-auth.ts
|
|
583
|
+
function createBrowserAuthSession(apiUrl) {
|
|
584
|
+
return new Promise((resolveSession, rejectSession) => {
|
|
585
|
+
let resolveToken;
|
|
586
|
+
let rejectToken;
|
|
587
|
+
const tokenPromise = new Promise((res, rej) => {
|
|
588
|
+
resolveToken = res;
|
|
589
|
+
rejectToken = rej;
|
|
590
|
+
});
|
|
591
|
+
let port = 0;
|
|
592
|
+
const nonce = crypto2.randomBytes(32).toString("base64url");
|
|
593
|
+
const server = http.createServer((req, res) => {
|
|
594
|
+
if (req.method === "GET" && req.url === "/login") {
|
|
595
|
+
const html = getLoginPageHtml(port, nonce);
|
|
596
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
597
|
+
res.end(html);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (req.method === "POST" && req.url === "/submit") {
|
|
601
|
+
let body = "";
|
|
602
|
+
req.on("data", (chunk) => {
|
|
603
|
+
body += chunk;
|
|
604
|
+
});
|
|
605
|
+
req.on("end", async () => {
|
|
606
|
+
try {
|
|
607
|
+
const parsed = JSON.parse(body);
|
|
608
|
+
const submittedNonce = typeof parsed.nonce === "string" ? parsed.nonce : "";
|
|
609
|
+
if (!safeEqual(submittedNonce, nonce)) {
|
|
610
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
611
|
+
res.end(JSON.stringify({ error: "INVALID_NONCE", message: "Open the login URL printed by the AI tool \u2014 that page is the only one allowed to submit credentials." }));
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
const { email, password } = parsed;
|
|
615
|
+
if (!email || !password) {
|
|
616
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
617
|
+
res.end(JSON.stringify({ error: "MISSING_FIELDS", message: "Email and password are required." }));
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const result = await emailAuth(apiUrl, email, password);
|
|
621
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
622
|
+
res.end(JSON.stringify({ ok: true, new: result.data.new, email: result.data.user.email }));
|
|
623
|
+
clearTimeout(timeout);
|
|
624
|
+
resolveToken({
|
|
625
|
+
token: result.token,
|
|
626
|
+
email: result.data.user.email,
|
|
627
|
+
isNewUser: result.data.new
|
|
628
|
+
});
|
|
629
|
+
} catch (err) {
|
|
630
|
+
const code = err?.code;
|
|
631
|
+
const status = err?.status || 500;
|
|
632
|
+
const message = code === "INVALID_CREDENTIALS" ? "That password doesn't match the account for this email." : code === "PASSWORD_TOO_SHORT" ? "Password must be at least 8 characters long." : code === "DISPOSABLE_EMAIL" ? "Please use a permanent email address. Disposable email providers are not allowed." : err?.message || "Something went wrong. Please try again.";
|
|
633
|
+
res.writeHead(status >= 400 && status < 500 ? status : 400, { "Content-Type": "application/json" });
|
|
634
|
+
res.end(JSON.stringify({ error: code || "AUTH_FAILED", message }));
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
res.writeHead(404);
|
|
640
|
+
res.end("Not found");
|
|
641
|
+
});
|
|
642
|
+
const timeout = setTimeout(() => {
|
|
643
|
+
server.close();
|
|
644
|
+
rejectToken(new Error("Login timed out after 5 minutes."));
|
|
645
|
+
}, 5 * 60 * 1e3);
|
|
646
|
+
server.listen(0, "127.0.0.1", () => {
|
|
647
|
+
const addr = server.address();
|
|
648
|
+
if (typeof addr === "object" && addr) {
|
|
649
|
+
port = addr.port;
|
|
650
|
+
}
|
|
651
|
+
resolveSession({
|
|
652
|
+
url: `http://localhost:${port}/login`,
|
|
653
|
+
port,
|
|
654
|
+
waitForToken: () => tokenPromise,
|
|
655
|
+
close: () => {
|
|
656
|
+
clearTimeout(timeout);
|
|
657
|
+
server.close();
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
server.on("error", (err) => {
|
|
662
|
+
rejectSession(err);
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
function safeEqual(a, b) {
|
|
667
|
+
const ab = Buffer.from(a);
|
|
668
|
+
const bb = Buffer.from(b);
|
|
669
|
+
if (ab.length !== bb.length)
|
|
670
|
+
return false;
|
|
671
|
+
return crypto2.timingSafeEqual(ab, bb);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// apps/mcp-server/src/utils/error-formatter.ts
|
|
675
|
+
var import_axios3 = require("axios");
|
|
676
|
+
function formatError(error) {
|
|
677
|
+
if (error instanceof import_axios3.AxiosError) {
|
|
678
|
+
const status = error.response?.status;
|
|
679
|
+
const data = error.response?.data;
|
|
680
|
+
const message = data?.message || data?.error || error.message;
|
|
681
|
+
switch (status) {
|
|
682
|
+
case 401:
|
|
683
|
+
return "Session expired. Please run auth_login again.";
|
|
684
|
+
case 403:
|
|
685
|
+
if (typeof message === "string" && message.toLowerCase().includes("limit")) {
|
|
686
|
+
return message;
|
|
687
|
+
}
|
|
688
|
+
return `Permission denied. ${message || "Check your role and workspace access."}`;
|
|
689
|
+
case 404:
|
|
690
|
+
return "Not found. Check the ID and try again.";
|
|
691
|
+
case 409:
|
|
692
|
+
return `Conflict: ${message}`;
|
|
693
|
+
case 422:
|
|
694
|
+
if (data?.errors && Array.isArray(data.errors)) {
|
|
695
|
+
return `Validation failed: ${data.errors.map((e) => e.message || e).join(", ")}`;
|
|
696
|
+
}
|
|
697
|
+
return `Validation failed: ${message}`;
|
|
698
|
+
default:
|
|
699
|
+
if (status && status >= 500) {
|
|
700
|
+
return "Server error. Try again in a moment.";
|
|
701
|
+
}
|
|
702
|
+
if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
|
|
703
|
+
return `Cannot reach the API. Check your connection and API URL.`;
|
|
704
|
+
}
|
|
705
|
+
return message || "An unexpected error occurred.";
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (error instanceof Error) {
|
|
709
|
+
return error.message;
|
|
710
|
+
}
|
|
711
|
+
return "An unexpected error occurred.";
|
|
712
|
+
}
|
|
713
|
+
function toolResponse(text) {
|
|
714
|
+
return { content: [{ type: "text", text }] };
|
|
715
|
+
}
|
|
716
|
+
function toolJson(data, prefix) {
|
|
717
|
+
const text = prefix ? `${prefix}
|
|
718
|
+
|
|
719
|
+
${JSON.stringify(data, null, 2)}` : JSON.stringify(data, null, 2);
|
|
720
|
+
return { content: [{ type: "text", text }] };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// apps/mcp-server/src/tools/auth.ts
|
|
724
|
+
var DASHBOARD_URL2 = "https://dashboard.hostingguru.io";
|
|
725
|
+
var activeAuthSession = null;
|
|
726
|
+
async function autoSelectWorkspace() {
|
|
727
|
+
try {
|
|
728
|
+
const workspaces = await listWorkspaces();
|
|
729
|
+
if (Array.isArray(workspaces) && workspaces.length === 1) {
|
|
730
|
+
session.setWorkspace(workspaces[0].id);
|
|
731
|
+
}
|
|
732
|
+
} catch {
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
function registerAuthTools(server) {
|
|
736
|
+
server.tool(
|
|
737
|
+
"auth_login",
|
|
738
|
+
"Sign in to HostingGuru. Opens a browser window for secure login \u2014 credentials never pass through the AI tool. New emails create a fresh account automatically (matches dashboard.hostingguru.io).",
|
|
739
|
+
{},
|
|
740
|
+
async () => {
|
|
741
|
+
try {
|
|
742
|
+
if (activeAuthSession) {
|
|
743
|
+
activeAuthSession.close();
|
|
744
|
+
activeAuthSession = null;
|
|
745
|
+
}
|
|
746
|
+
const authSession = await createBrowserAuthSession(session.apiUrl);
|
|
747
|
+
activeAuthSession = authSession;
|
|
748
|
+
authSession.waitForToken().then(async ({ token }) => {
|
|
749
|
+
session.setAuth(token);
|
|
750
|
+
resetClient();
|
|
751
|
+
await autoSelectWorkspace();
|
|
752
|
+
saveCredentials();
|
|
753
|
+
activeAuthSession = null;
|
|
754
|
+
}).catch(() => {
|
|
755
|
+
activeAuthSession = null;
|
|
756
|
+
});
|
|
757
|
+
return toolResponse(
|
|
758
|
+
`Open this URL in your browser to sign in:
|
|
759
|
+
|
|
760
|
+
${authSession.url}
|
|
761
|
+
|
|
762
|
+
Already on the dashboard? Run auth_status to confirm once you have signed in here.
|
|
763
|
+
Using Google or GitHub OAuth? Use auth_token with a session token copied from the dashboard.`
|
|
764
|
+
);
|
|
765
|
+
} catch (error) {
|
|
766
|
+
return toolResponse(formatError(error));
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
);
|
|
770
|
+
server.tool(
|
|
771
|
+
"auth_token",
|
|
772
|
+
`Authenticate with an existing session token. Use this if you signed in to ${DASHBOARD_URL2} via Google or GitHub \u2014 copy the value of the \`token\` cookie from your browser's devtools and paste it here.`,
|
|
773
|
+
{ token: import_zod.z.string().min(1) },
|
|
774
|
+
async ({ token }) => {
|
|
775
|
+
try {
|
|
776
|
+
session.setAuth(token);
|
|
777
|
+
resetClient();
|
|
778
|
+
await autoSelectWorkspace();
|
|
779
|
+
saveCredentials();
|
|
780
|
+
const wsInfo = session.workspaceId ? `Workspace auto-selected: ${session.workspaceId}` : "No workspace selected yet. Use list_workspaces and select_workspace.";
|
|
781
|
+
return toolResponse(`Authenticated successfully.
|
|
782
|
+
${wsInfo}`);
|
|
783
|
+
} catch (error) {
|
|
784
|
+
return toolResponse(formatError(error));
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
);
|
|
788
|
+
server.tool(
|
|
789
|
+
"auth_status",
|
|
790
|
+
"Check current authentication state and selected workspace.",
|
|
791
|
+
{},
|
|
792
|
+
async () => {
|
|
793
|
+
if (!session.isAuthenticated()) {
|
|
794
|
+
return toolResponse("Not authenticated. Use auth_login to sign in via browser.");
|
|
795
|
+
}
|
|
796
|
+
return toolJson({
|
|
797
|
+
authenticated: true,
|
|
798
|
+
apiUrl: session.apiUrl,
|
|
799
|
+
workspaceId: session.workspaceId || "none \u2014 use select_workspace"
|
|
800
|
+
}, "Current session:");
|
|
801
|
+
}
|
|
802
|
+
);
|
|
803
|
+
server.tool(
|
|
804
|
+
"auth_logout",
|
|
805
|
+
"Sign out: clears the local session and invalidates the cookie on the backend.",
|
|
806
|
+
{},
|
|
807
|
+
async () => {
|
|
808
|
+
try {
|
|
809
|
+
if (session.isAuthenticated()) {
|
|
810
|
+
await logout();
|
|
811
|
+
}
|
|
812
|
+
clearCredentials();
|
|
813
|
+
resetClient();
|
|
814
|
+
return toolResponse("Signed out. Use auth_login to sign in again.");
|
|
815
|
+
} catch (error) {
|
|
816
|
+
clearCredentials();
|
|
817
|
+
resetClient();
|
|
818
|
+
return toolResponse(`Signed out locally. (${formatError(error)})`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
);
|
|
822
|
+
server.tool(
|
|
823
|
+
"auth_signup",
|
|
824
|
+
"Create a new HostingGuru account. The unified login flow signs you up automatically when the email is new \u2014 use auth_login instead. This tool just points you at the dashboard for OAuth signup.",
|
|
825
|
+
{},
|
|
826
|
+
async () => {
|
|
827
|
+
return toolResponse(
|
|
828
|
+
`New email + password? Use auth_login \u2014 accounts are created on the fly when the email is unknown.
|
|
829
|
+
|
|
830
|
+
Prefer Google or GitHub? Sign up at ${DASHBOARD_URL2}/auth, then copy your session cookie and pass it to auth_token.`
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// apps/mcp-server/src/tools/workspaces.ts
|
|
837
|
+
var import_zod2 = require("zod");
|
|
838
|
+
function registerWorkspaceTools(server) {
|
|
839
|
+
server.tool(
|
|
840
|
+
"list_workspaces",
|
|
841
|
+
"List all workspaces you belong to.",
|
|
842
|
+
{},
|
|
843
|
+
async () => {
|
|
844
|
+
try {
|
|
845
|
+
const data = await listWorkspaces();
|
|
846
|
+
if (Array.isArray(data) && data.length === 0) {
|
|
847
|
+
return toolResponse("No workspaces found. Create one with create_workspace.");
|
|
848
|
+
}
|
|
849
|
+
return toolJson(data, "Your workspaces:");
|
|
850
|
+
} catch (error) {
|
|
851
|
+
return toolResponse(formatError(error));
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
);
|
|
855
|
+
server.tool(
|
|
856
|
+
"create_workspace",
|
|
857
|
+
"Create a new workspace and auto-select it.",
|
|
858
|
+
{ name: import_zod2.z.string().min(1), slug: import_zod2.z.string().optional() },
|
|
859
|
+
async ({ name, slug }) => {
|
|
860
|
+
try {
|
|
861
|
+
const data = await createWorkspace(name, slug);
|
|
862
|
+
session.setWorkspace(data.id);
|
|
863
|
+
saveCredentials();
|
|
864
|
+
return toolJson(data, `Workspace created and selected.`);
|
|
865
|
+
} catch (error) {
|
|
866
|
+
return toolResponse(formatError(error));
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
);
|
|
870
|
+
server.tool(
|
|
871
|
+
"select_workspace",
|
|
872
|
+
"Select an active workspace by ID. All subsequent operations use this workspace.",
|
|
873
|
+
{ workspaceId: import_zod2.z.string().uuid() },
|
|
874
|
+
async ({ workspaceId }) => {
|
|
875
|
+
try {
|
|
876
|
+
session.setWorkspace(workspaceId);
|
|
877
|
+
saveCredentials();
|
|
878
|
+
return toolResponse(`Workspace selected: ${workspaceId}`);
|
|
879
|
+
} catch (error) {
|
|
880
|
+
return toolResponse(formatError(error));
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// apps/mcp-server/src/tools/github.ts
|
|
887
|
+
var import_zod3 = require("zod");
|
|
888
|
+
|
|
889
|
+
// apps/mcp-server/src/api/github.ts
|
|
890
|
+
async function connectGithub(installationId) {
|
|
891
|
+
const res = await getClient().post("/github", { installationId });
|
|
892
|
+
return res.data;
|
|
893
|
+
}
|
|
894
|
+
async function listRepositories() {
|
|
895
|
+
const res = await getClient().get("/github/repositories");
|
|
896
|
+
return res.data;
|
|
897
|
+
}
|
|
898
|
+
async function listBranches(repositoryId) {
|
|
899
|
+
const res = await getClient().get(`/github/repositories/${repositoryId}/branches`);
|
|
900
|
+
return res.data;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// apps/mcp-server/src/tools/github.ts
|
|
904
|
+
function registerGitHubTools(server) {
|
|
905
|
+
server.tool(
|
|
906
|
+
"connect_github",
|
|
907
|
+
"Connect a GitHub account. If no installationId, returns the GitHub App install URL to open in browser.",
|
|
908
|
+
{ installationId: import_zod3.z.string().optional() },
|
|
909
|
+
async ({ installationId }) => {
|
|
910
|
+
try {
|
|
911
|
+
if (!installationId) {
|
|
912
|
+
return toolResponse(
|
|
913
|
+
"To connect GitHub, install the Hostingguru GitHub App:\n\nhttps://github.com/apps/hostingguru/installations/new\n\nAfter installing, the browser will redirect with an installation_id. Run connect_github again with that installationId."
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
const data = await connectGithub(installationId);
|
|
917
|
+
return toolJson(data, "GitHub connected successfully.");
|
|
918
|
+
} catch (error) {
|
|
919
|
+
return toolResponse(formatError(error));
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
);
|
|
923
|
+
server.tool(
|
|
924
|
+
"list_repositories",
|
|
925
|
+
"List GitHub repositories available for deployment.",
|
|
926
|
+
{},
|
|
927
|
+
async () => {
|
|
928
|
+
try {
|
|
929
|
+
const data = await listRepositories();
|
|
930
|
+
if (Array.isArray(data) && data.length === 0) {
|
|
931
|
+
return toolResponse("No repositories found. Connect GitHub first with connect_github.");
|
|
932
|
+
}
|
|
933
|
+
return toolJson(data, "Available repositories:");
|
|
934
|
+
} catch (error) {
|
|
935
|
+
return toolResponse(formatError(error));
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
);
|
|
939
|
+
server.tool(
|
|
940
|
+
"list_branches",
|
|
941
|
+
"List branches for a GitHub repository.",
|
|
942
|
+
{ repositoryId: import_zod3.z.string() },
|
|
943
|
+
async ({ repositoryId }) => {
|
|
944
|
+
try {
|
|
945
|
+
const data = await listBranches(repositoryId);
|
|
946
|
+
return toolJson(data, "Branches:");
|
|
947
|
+
} catch (error) {
|
|
948
|
+
return toolResponse(formatError(error));
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// apps/mcp-server/src/tools/deployments.ts
|
|
955
|
+
var import_zod4 = require("zod");
|
|
956
|
+
|
|
957
|
+
// apps/mcp-server/src/api/deployments.ts
|
|
958
|
+
async function createDeployment(params) {
|
|
959
|
+
const res = await getClient().post("/deployments", params);
|
|
960
|
+
return res.data;
|
|
961
|
+
}
|
|
962
|
+
async function listDeployments(projectId) {
|
|
963
|
+
const params = projectId ? { projectId } : {};
|
|
964
|
+
const res = await getClient().get("/deployments", { params });
|
|
965
|
+
return res.data;
|
|
966
|
+
}
|
|
967
|
+
async function getDeployment(id) {
|
|
968
|
+
const res = await getClient().get(`/deployments/${id}`);
|
|
969
|
+
return res.data;
|
|
970
|
+
}
|
|
971
|
+
async function getLogs(id) {
|
|
972
|
+
const res = await getClient().get(`/deployments/${id}/logs`);
|
|
973
|
+
return res.data;
|
|
974
|
+
}
|
|
975
|
+
async function redeploy(id) {
|
|
976
|
+
const res = await getClient().post(`/deployments/${id}/redeploy`);
|
|
977
|
+
return res.data;
|
|
978
|
+
}
|
|
979
|
+
async function stopDeployment(id) {
|
|
980
|
+
const res = await getClient().post(`/deployments/${id}/stop`);
|
|
981
|
+
return res.data;
|
|
982
|
+
}
|
|
983
|
+
async function startDeployment(id) {
|
|
984
|
+
const res = await getClient().post(`/deployments/${id}/start`);
|
|
985
|
+
return res.data;
|
|
986
|
+
}
|
|
987
|
+
async function setEnvVars(id, variables, isSecret) {
|
|
988
|
+
const payload = Object.entries(variables).map(([key, value]) => ({
|
|
989
|
+
key,
|
|
990
|
+
value,
|
|
991
|
+
isSecret: isSecret ?? false
|
|
992
|
+
}));
|
|
993
|
+
const res = await getClient().post(`/deployments/${id}/env/bulk`, { variables: payload });
|
|
994
|
+
return res.data;
|
|
995
|
+
}
|
|
996
|
+
async function listEnvVars(id) {
|
|
997
|
+
const res = await getClient().get(`/deployments/${id}/env`);
|
|
998
|
+
return res.data;
|
|
999
|
+
}
|
|
1000
|
+
async function listBuilds(id) {
|
|
1001
|
+
const res = await getClient().get(`/deployments/${id}/builds`);
|
|
1002
|
+
return res.data;
|
|
1003
|
+
}
|
|
1004
|
+
async function getBuildLogs(id, buildId) {
|
|
1005
|
+
const res = await getClient().get(`/deployments/${id}/builds/${buildId}/logs`);
|
|
1006
|
+
return res.data;
|
|
1007
|
+
}
|
|
1008
|
+
async function rollbackDeployment(id, commitSha) {
|
|
1009
|
+
const res = await getClient().post(`/deployments/${id}/rollback`, { commitSha });
|
|
1010
|
+
return res.data;
|
|
1011
|
+
}
|
|
1012
|
+
async function deleteEnvVar(id, key) {
|
|
1013
|
+
const res = await getClient().delete(`/deployments/${id}/env/${key}`);
|
|
1014
|
+
return res.data;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// apps/mcp-server/src/utils/framework-detector.ts
|
|
1018
|
+
function detectFramework(repoName, language) {
|
|
1019
|
+
const name = repoName.toLowerCase();
|
|
1020
|
+
if (name.includes("next")) {
|
|
1021
|
+
return { framework: "nextjs", type: "api", buildCommand: "npm run build", startCommand: "npm start" };
|
|
1022
|
+
}
|
|
1023
|
+
if (name.includes("react") || name.includes("vite")) {
|
|
1024
|
+
return { framework: "react", type: "static_site", buildCommand: "npm run build" };
|
|
1025
|
+
}
|
|
1026
|
+
if (name.includes("vue") || name.includes("nuxt")) {
|
|
1027
|
+
return { framework: "node", type: "api", buildCommand: "npm run build", startCommand: "npm start" };
|
|
1028
|
+
}
|
|
1029
|
+
const lang = (language || "").toLowerCase();
|
|
1030
|
+
if (lang === "python" && name.includes("django")) {
|
|
1031
|
+
return { framework: "django", type: "api", buildCommand: "pip install -r requirements.txt", startCommand: "python manage.py runserver 0.0.0.0:8000" };
|
|
1032
|
+
}
|
|
1033
|
+
if (lang === "python" && name.includes("flask")) {
|
|
1034
|
+
return { framework: "python", type: "api", buildCommand: "pip install -r requirements.txt", startCommand: "python app.py" };
|
|
1035
|
+
}
|
|
1036
|
+
if (lang === "python") {
|
|
1037
|
+
return { framework: "python", type: "api", buildCommand: "pip install -r requirements.txt", startCommand: "python main.py" };
|
|
1038
|
+
}
|
|
1039
|
+
if (lang === "go") {
|
|
1040
|
+
return { framework: "go", type: "api", buildCommand: "go build -o app", startCommand: "./app" };
|
|
1041
|
+
}
|
|
1042
|
+
if (lang === "javascript" || lang === "typescript") {
|
|
1043
|
+
return { framework: "node", type: "api", buildCommand: "npm install", startCommand: "npm start" };
|
|
1044
|
+
}
|
|
1045
|
+
return { framework: "static", type: "static_site" };
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// apps/mcp-server/src/tools/deployments.ts
|
|
1049
|
+
var VALID_FRAMEWORKS = ["node", "nextjs", "react", "python", "django", "go", "static"];
|
|
1050
|
+
var VALID_TYPES = ["api", "static_site", "worker", "script"];
|
|
1051
|
+
function registerDeploymentTools(server) {
|
|
1052
|
+
server.tool(
|
|
1053
|
+
"deploy",
|
|
1054
|
+
"Deploy a repository. Fuzzy-matches repo name, auto-detects framework, applies smart defaults. The flagship deployment tool.",
|
|
1055
|
+
{
|
|
1056
|
+
repository: import_zod4.z.string().describe("Repository name or partial match"),
|
|
1057
|
+
branch: import_zod4.z.string().optional().describe("Branch to deploy (default: main/master)"),
|
|
1058
|
+
name: import_zod4.z.string().optional().describe("Deployment name"),
|
|
1059
|
+
type: import_zod4.z.enum(VALID_TYPES).optional().describe("Deployment type"),
|
|
1060
|
+
framework: import_zod4.z.enum(VALID_FRAMEWORKS).optional().describe("Framework"),
|
|
1061
|
+
buildCommand: import_zod4.z.string().optional(),
|
|
1062
|
+
startCommand: import_zod4.z.string().optional(),
|
|
1063
|
+
rootDirectory: import_zod4.z.string().optional(),
|
|
1064
|
+
publishDirectory: import_zod4.z.string().optional(),
|
|
1065
|
+
autoDeploy: import_zod4.z.boolean().optional().describe("Auto-deploy on push (default: true)"),
|
|
1066
|
+
envVars: import_zod4.z.record(import_zod4.z.string(), import_zod4.z.string()).optional().describe("Environment variables to set")
|
|
1067
|
+
},
|
|
1068
|
+
async (params) => {
|
|
1069
|
+
try {
|
|
1070
|
+
const repos = await listRepositories();
|
|
1071
|
+
if (!Array.isArray(repos) || repos.length === 0) {
|
|
1072
|
+
return toolResponse("No repositories found. Connect GitHub first with connect_github.");
|
|
1073
|
+
}
|
|
1074
|
+
const query = params.repository.toLowerCase();
|
|
1075
|
+
let matched = repos.find((r) => r.fullName?.toLowerCase() === query || r.name?.toLowerCase() === query);
|
|
1076
|
+
if (!matched) {
|
|
1077
|
+
matched = repos.find(
|
|
1078
|
+
(r) => r.fullName?.toLowerCase().includes(query) || r.name?.toLowerCase().includes(query)
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
if (!matched) {
|
|
1082
|
+
const repoNames = repos.map((r) => r.fullName || r.name).join("\n ");
|
|
1083
|
+
return toolResponse(`Repository "${params.repository}" not found. Available:
|
|
1084
|
+
${repoNames}`);
|
|
1085
|
+
}
|
|
1086
|
+
const defaults = detectFramework(matched.name || "", matched.language);
|
|
1087
|
+
const deployParams = {
|
|
1088
|
+
repositoryId: matched.id,
|
|
1089
|
+
repositoryFullName: matched.fullName || matched.name,
|
|
1090
|
+
branch: params.branch || matched.defaultBranch || "main",
|
|
1091
|
+
name: params.name || matched.name,
|
|
1092
|
+
type: params.type || defaults.type,
|
|
1093
|
+
framework: params.framework || defaults.framework,
|
|
1094
|
+
buildCommand: params.buildCommand || defaults.buildCommand,
|
|
1095
|
+
startCommand: params.startCommand || defaults.startCommand,
|
|
1096
|
+
rootDirectory: params.rootDirectory,
|
|
1097
|
+
publishDirectory: params.publishDirectory,
|
|
1098
|
+
autoDeploy: params.autoDeploy ?? true
|
|
1099
|
+
};
|
|
1100
|
+
const deployment = await createDeployment(deployParams);
|
|
1101
|
+
if (params.envVars && Object.keys(params.envVars).length > 0) {
|
|
1102
|
+
try {
|
|
1103
|
+
await setEnvVars(deployment.id, params.envVars);
|
|
1104
|
+
} catch {
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
return toolJson(
|
|
1108
|
+
deployment,
|
|
1109
|
+
`Deployment created! Status: ${deployment.status || "pending"}
|
|
1110
|
+
Framework: ${deployParams.framework} | Type: ${deployParams.type}
|
|
1111
|
+
Use get_deployment with id "${deployment.id}" to check progress.`
|
|
1112
|
+
);
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
return toolResponse(formatError(error));
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
);
|
|
1118
|
+
server.tool(
|
|
1119
|
+
"list_deployments",
|
|
1120
|
+
"List all deployments, optionally filtered by project.",
|
|
1121
|
+
{ projectId: import_zod4.z.string().optional() },
|
|
1122
|
+
async ({ projectId }) => {
|
|
1123
|
+
try {
|
|
1124
|
+
const data = await listDeployments(projectId);
|
|
1125
|
+
if (Array.isArray(data) && data.length === 0) {
|
|
1126
|
+
return toolResponse("No deployments found. Use deploy to create one.");
|
|
1127
|
+
}
|
|
1128
|
+
return toolJson(data, "Deployments:");
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
return toolResponse(formatError(error));
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
);
|
|
1134
|
+
server.tool(
|
|
1135
|
+
"get_deployment",
|
|
1136
|
+
"Get deployment details including status and live URL.",
|
|
1137
|
+
{ deploymentId: import_zod4.z.string() },
|
|
1138
|
+
async ({ deploymentId }) => {
|
|
1139
|
+
try {
|
|
1140
|
+
const data = await getDeployment(deploymentId);
|
|
1141
|
+
return toolJson(data);
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
return toolResponse(formatError(error));
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
);
|
|
1147
|
+
server.tool(
|
|
1148
|
+
"get_logs",
|
|
1149
|
+
"Get application logs for a deployment.",
|
|
1150
|
+
{ deploymentId: import_zod4.z.string() },
|
|
1151
|
+
async ({ deploymentId }) => {
|
|
1152
|
+
try {
|
|
1153
|
+
const data = await getLogs(deploymentId);
|
|
1154
|
+
if (typeof data === "string") {
|
|
1155
|
+
return toolResponse(data || "No logs available yet.");
|
|
1156
|
+
}
|
|
1157
|
+
return toolJson(data);
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
return toolResponse(formatError(error));
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
);
|
|
1163
|
+
server.tool(
|
|
1164
|
+
"list_builds",
|
|
1165
|
+
"List builds for a deployment.",
|
|
1166
|
+
{ deploymentId: import_zod4.z.string() },
|
|
1167
|
+
async ({ deploymentId }) => {
|
|
1168
|
+
try {
|
|
1169
|
+
const data = await listBuilds(deploymentId);
|
|
1170
|
+
if (Array.isArray(data) && data.length === 0) {
|
|
1171
|
+
return toolResponse("No builds found yet.");
|
|
1172
|
+
}
|
|
1173
|
+
return toolJson(data, "Builds:");
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
return toolResponse(formatError(error));
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
);
|
|
1179
|
+
server.tool(
|
|
1180
|
+
"get_build_logs",
|
|
1181
|
+
"Get build/deploy logs for a specific build.",
|
|
1182
|
+
{ deploymentId: import_zod4.z.string(), buildId: import_zod4.z.string() },
|
|
1183
|
+
async ({ deploymentId, buildId }) => {
|
|
1184
|
+
try {
|
|
1185
|
+
const data = await getBuildLogs(deploymentId, buildId);
|
|
1186
|
+
if (typeof data === "string") {
|
|
1187
|
+
return toolResponse(data || "No build logs available yet.");
|
|
1188
|
+
}
|
|
1189
|
+
return toolJson(data);
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
return toolResponse(formatError(error));
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
);
|
|
1195
|
+
server.tool(
|
|
1196
|
+
"redeploy",
|
|
1197
|
+
"Trigger a redeployment.",
|
|
1198
|
+
{ deploymentId: import_zod4.z.string() },
|
|
1199
|
+
async ({ deploymentId }) => {
|
|
1200
|
+
try {
|
|
1201
|
+
const data = await redeploy(deploymentId);
|
|
1202
|
+
return toolResponse(`Redeployment triggered. Use get_deployment to track progress.`);
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
return toolResponse(formatError(error));
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
);
|
|
1208
|
+
server.tool(
|
|
1209
|
+
"rollback_deployment",
|
|
1210
|
+
"Rollback a deployment to a previous successful build commit. Use list_builds to find available commits.",
|
|
1211
|
+
{
|
|
1212
|
+
deploymentId: import_zod4.z.string(),
|
|
1213
|
+
commitSha: import_zod4.z.string().describe("The commit SHA to rollback to (from list_builds)")
|
|
1214
|
+
},
|
|
1215
|
+
async ({ deploymentId, commitSha }) => {
|
|
1216
|
+
try {
|
|
1217
|
+
const data = await rollbackDeployment(deploymentId, commitSha);
|
|
1218
|
+
return toolResponse(`Rollback to ${commitSha.slice(0, 7)} triggered. Use get_deployment to track progress.`);
|
|
1219
|
+
} catch (error) {
|
|
1220
|
+
return toolResponse(formatError(error));
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
);
|
|
1224
|
+
server.tool(
|
|
1225
|
+
"stop_deployment",
|
|
1226
|
+
"Stop a running deployment.",
|
|
1227
|
+
{ deploymentId: import_zod4.z.string() },
|
|
1228
|
+
async ({ deploymentId }) => {
|
|
1229
|
+
try {
|
|
1230
|
+
await stopDeployment(deploymentId);
|
|
1231
|
+
return toolResponse("Deployment stopped.");
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
return toolResponse(formatError(error));
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
);
|
|
1237
|
+
server.tool(
|
|
1238
|
+
"start_deployment",
|
|
1239
|
+
"Start a stopped deployment.",
|
|
1240
|
+
{ deploymentId: import_zod4.z.string() },
|
|
1241
|
+
async ({ deploymentId }) => {
|
|
1242
|
+
try {
|
|
1243
|
+
await startDeployment(deploymentId);
|
|
1244
|
+
return toolResponse("Deployment started. Use get_deployment to track progress.");
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
return toolResponse(formatError(error));
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// apps/mcp-server/src/tools/env.ts
|
|
1253
|
+
var import_zod5 = require("zod");
|
|
1254
|
+
function registerEnvTools(server) {
|
|
1255
|
+
server.tool(
|
|
1256
|
+
"set_env_vars",
|
|
1257
|
+
"Set environment variables on a deployment. Replaces all existing vars.",
|
|
1258
|
+
{
|
|
1259
|
+
deploymentId: import_zod5.z.string(),
|
|
1260
|
+
variables: import_zod5.z.record(import_zod5.z.string(), import_zod5.z.string()).describe("Key-value pairs of environment variables"),
|
|
1261
|
+
isSecret: import_zod5.z.boolean().optional().describe("Mark variables as secret (default: false)")
|
|
1262
|
+
},
|
|
1263
|
+
async ({ deploymentId, variables, isSecret }) => {
|
|
1264
|
+
try {
|
|
1265
|
+
await setEnvVars(deploymentId, variables, isSecret);
|
|
1266
|
+
const count = Object.keys(variables).length;
|
|
1267
|
+
return toolResponse(`${count} environment variable${count !== 1 ? "s" : ""} set.`);
|
|
1268
|
+
} catch (error) {
|
|
1269
|
+
return toolResponse(formatError(error));
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
);
|
|
1273
|
+
server.tool(
|
|
1274
|
+
"list_env_vars",
|
|
1275
|
+
"List environment variables for a deployment (values may be masked).",
|
|
1276
|
+
{ deploymentId: import_zod5.z.string() },
|
|
1277
|
+
async ({ deploymentId }) => {
|
|
1278
|
+
try {
|
|
1279
|
+
const data = await listEnvVars(deploymentId);
|
|
1280
|
+
if (Array.isArray(data) && data.length === 0) {
|
|
1281
|
+
return toolResponse("No environment variables set.");
|
|
1282
|
+
}
|
|
1283
|
+
return toolJson(data, "Environment variables:");
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
return toolResponse(formatError(error));
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
);
|
|
1289
|
+
server.tool(
|
|
1290
|
+
"delete_env_var",
|
|
1291
|
+
"Delete an environment variable by key.",
|
|
1292
|
+
{ deploymentId: import_zod5.z.string(), key: import_zod5.z.string() },
|
|
1293
|
+
async ({ deploymentId, key }) => {
|
|
1294
|
+
try {
|
|
1295
|
+
await deleteEnvVar(deploymentId, key);
|
|
1296
|
+
return toolResponse(`Environment variable "${key}" deleted.`);
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
return toolResponse(formatError(error));
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// apps/mcp-server/src/api/billing.ts
|
|
1305
|
+
async function getSubscription() {
|
|
1306
|
+
const res = await getClient().get("/billing/subscription");
|
|
1307
|
+
return res.data;
|
|
1308
|
+
}
|
|
1309
|
+
async function getUsage() {
|
|
1310
|
+
const res = await getClient().get("/billing/usage");
|
|
1311
|
+
return res.data;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// apps/mcp-server/src/tools/billing.ts
|
|
1315
|
+
function registerBillingTools(server) {
|
|
1316
|
+
server.tool(
|
|
1317
|
+
"get_subscription",
|
|
1318
|
+
"Get current subscription plan, status, and usage limits.",
|
|
1319
|
+
{},
|
|
1320
|
+
async () => {
|
|
1321
|
+
try {
|
|
1322
|
+
const [subscription, usage] = await Promise.all([
|
|
1323
|
+
getSubscription(),
|
|
1324
|
+
getUsage()
|
|
1325
|
+
]);
|
|
1326
|
+
return toolJson({ subscription, usage }, "Billing overview:");
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
return toolResponse(formatError(error));
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// apps/mcp-server/src/tools/domains.ts
|
|
1335
|
+
var import_zod6 = require("zod");
|
|
1336
|
+
|
|
1337
|
+
// apps/mcp-server/src/api/domains.ts
|
|
1338
|
+
async function attachDomain(deploymentId, domain) {
|
|
1339
|
+
const res = await getClient().post(`/deployments/${deploymentId}/domains`, { domain });
|
|
1340
|
+
return res.data;
|
|
1341
|
+
}
|
|
1342
|
+
async function verifyDomain(deploymentId, domainId) {
|
|
1343
|
+
const res = await getClient().post(`/deployments/${deploymentId}/domains/${domainId}/verify`);
|
|
1344
|
+
return res.data;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// apps/mcp-server/src/tools/domains.ts
|
|
1348
|
+
function registerDomainTools(server) {
|
|
1349
|
+
server.tool(
|
|
1350
|
+
"attach_domain",
|
|
1351
|
+
"Attach a custom domain to a deployment. Returns DNS instructions.",
|
|
1352
|
+
{ deploymentId: import_zod6.z.string(), domain: import_zod6.z.string() },
|
|
1353
|
+
async ({ deploymentId, domain }) => {
|
|
1354
|
+
try {
|
|
1355
|
+
const data = await attachDomain(deploymentId, domain);
|
|
1356
|
+
return toolJson(
|
|
1357
|
+
data,
|
|
1358
|
+
`Domain "${domain}" attached.
|
|
1359
|
+
|
|
1360
|
+
Next step: Add a CNAME record pointing to the provided target, then run verify_domain.`
|
|
1361
|
+
);
|
|
1362
|
+
} catch (error) {
|
|
1363
|
+
return toolResponse(formatError(error));
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
);
|
|
1367
|
+
server.tool(
|
|
1368
|
+
"verify_domain",
|
|
1369
|
+
"Verify DNS configuration for an attached domain.",
|
|
1370
|
+
{ deploymentId: import_zod6.z.string(), domainId: import_zod6.z.string() },
|
|
1371
|
+
async ({ deploymentId, domainId }) => {
|
|
1372
|
+
try {
|
|
1373
|
+
const data = await verifyDomain(deploymentId, domainId);
|
|
1374
|
+
return toolJson(data, "Domain verification result:");
|
|
1375
|
+
} catch (error) {
|
|
1376
|
+
return toolResponse(formatError(error));
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// apps/mcp-server/src/tools/projects.ts
|
|
1383
|
+
var import_zod7 = require("zod");
|
|
1384
|
+
|
|
1385
|
+
// apps/mcp-server/src/api/projects.ts
|
|
1386
|
+
async function createProject(name) {
|
|
1387
|
+
const res = await getClient().post("/projects", { name });
|
|
1388
|
+
return res.data;
|
|
1389
|
+
}
|
|
1390
|
+
async function listProjects() {
|
|
1391
|
+
const res = await getClient().get("/projects");
|
|
1392
|
+
return res.data;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// apps/mcp-server/src/tools/projects.ts
|
|
1396
|
+
function registerProjectTools(server) {
|
|
1397
|
+
server.tool(
|
|
1398
|
+
"create_project",
|
|
1399
|
+
"Create a project to group deployments.",
|
|
1400
|
+
{ name: import_zod7.z.string().min(1) },
|
|
1401
|
+
async ({ name }) => {
|
|
1402
|
+
try {
|
|
1403
|
+
const data = await createProject(name);
|
|
1404
|
+
return toolJson(data, "Project created.");
|
|
1405
|
+
} catch (error) {
|
|
1406
|
+
return toolResponse(formatError(error));
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
);
|
|
1410
|
+
server.tool(
|
|
1411
|
+
"list_projects",
|
|
1412
|
+
"List all projects in the workspace.",
|
|
1413
|
+
{},
|
|
1414
|
+
async () => {
|
|
1415
|
+
try {
|
|
1416
|
+
const data = await listProjects();
|
|
1417
|
+
if (Array.isArray(data) && data.length === 0) {
|
|
1418
|
+
return toolResponse("No projects found. Use create_project to create one.");
|
|
1419
|
+
}
|
|
1420
|
+
return toolJson(data, "Projects:");
|
|
1421
|
+
} catch (error) {
|
|
1422
|
+
return toolResponse(formatError(error));
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// apps/mcp-server/src/main.ts
|
|
1429
|
+
async function main() {
|
|
1430
|
+
loadCredentials();
|
|
1431
|
+
const server = new import_mcp.McpServer({
|
|
1432
|
+
name: "hostingguru",
|
|
1433
|
+
version: CONFIG.version
|
|
1434
|
+
});
|
|
1435
|
+
registerAuthTools(server);
|
|
1436
|
+
registerWorkspaceTools(server);
|
|
1437
|
+
registerGitHubTools(server);
|
|
1438
|
+
registerDeploymentTools(server);
|
|
1439
|
+
registerEnvTools(server);
|
|
1440
|
+
registerBillingTools(server);
|
|
1441
|
+
registerDomainTools(server);
|
|
1442
|
+
registerProjectTools(server);
|
|
1443
|
+
const transport = new import_stdio.StdioServerTransport();
|
|
1444
|
+
await server.connect(transport);
|
|
1445
|
+
}
|
|
1446
|
+
main().catch((error) => {
|
|
1447
|
+
console.error("Fatal error:", error);
|
|
1448
|
+
process.exit(1);
|
|
1449
|
+
});
|