@spacelr/mcp 0.0.5 → 0.0.8
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/dist/index.mjs +1557 -125
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -5,21 +5,89 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
5
5
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
6
|
|
|
7
7
|
// libs/mcp-server/src/config.ts
|
|
8
|
+
import * as crypto from "crypto";
|
|
8
9
|
import * as fs from "fs";
|
|
9
10
|
import * as os from "os";
|
|
10
11
|
import * as path from "path";
|
|
11
12
|
var REFRESH_TIMEOUT_MS = 1e4;
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
var SPACELR_CONFIG_FILENAME = "spacelr.json";
|
|
14
|
+
function getGlobalCredentialsFile() {
|
|
15
|
+
const home = process.env["HOME"] ?? process.env["USERPROFILE"] ?? os.homedir();
|
|
14
16
|
return path.join(home, ".spacelr", "credentials.json");
|
|
15
17
|
}
|
|
16
|
-
function
|
|
18
|
+
function findSpacelrConfigPath(startDir) {
|
|
19
|
+
let dir = startDir ?? process.cwd();
|
|
20
|
+
while (true) {
|
|
21
|
+
const candidate = path.join(dir, SPACELR_CONFIG_FILENAME);
|
|
22
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
23
|
+
const parent = path.dirname(dir);
|
|
24
|
+
if (parent === dir) return null;
|
|
25
|
+
dir = parent;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function resolveCredentialsFile() {
|
|
29
|
+
const configPath = findSpacelrConfigPath();
|
|
30
|
+
if (configPath) {
|
|
31
|
+
const projectDir = path.dirname(configPath);
|
|
32
|
+
return {
|
|
33
|
+
path: path.join(projectDir, ".spacelr", "credentials.json"),
|
|
34
|
+
scope: "project",
|
|
35
|
+
projectDir
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return { path: getGlobalCredentialsFile(), scope: "global" };
|
|
39
|
+
}
|
|
40
|
+
function ensureGitignored(projectDir) {
|
|
41
|
+
const gitignorePath = path.join(projectDir, ".gitignore");
|
|
42
|
+
const entry = ".spacelr/";
|
|
43
|
+
const matchesExisting = (line) => {
|
|
44
|
+
const trimmed = line.trim();
|
|
45
|
+
if (!trimmed || trimmed.startsWith("#")) return false;
|
|
46
|
+
return trimmed === ".spacelr" || trimmed === ".spacelr/" || trimmed === ".spacelr/*" || trimmed === ".spacelr/**" || trimmed === ".spacelr/credentials.json" || // Leading-slash forms anchor the pattern to the repo root
|
|
47
|
+
trimmed === "/.spacelr" || trimmed === "/.spacelr/" || trimmed === "/.spacelr/*" || trimmed === "/.spacelr/**" || trimmed === "/.spacelr/credentials.json";
|
|
48
|
+
};
|
|
49
|
+
const hasNegation = (line) => {
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
if (!trimmed || trimmed.startsWith("#")) return false;
|
|
52
|
+
return trimmed.startsWith("!") && trimmed.includes(".spacelr");
|
|
53
|
+
};
|
|
54
|
+
let content = "";
|
|
55
|
+
let existed = false;
|
|
56
|
+
try {
|
|
57
|
+
content = fs.readFileSync(gitignorePath, "utf-8");
|
|
58
|
+
existed = true;
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (err.code !== "ENOENT") return { safe: true };
|
|
61
|
+
}
|
|
62
|
+
const lines = content.split(/\r?\n/);
|
|
63
|
+
if (lines.some(hasNegation)) {
|
|
64
|
+
const alreadyHasMatch = lines.some(matchesExisting);
|
|
65
|
+
console.error(
|
|
66
|
+
alreadyHasMatch ? ".gitignore already contains .spacelr/ but ALSO contains a negation pattern that un-ignores credentials. Remove the negation or store credentials globally." : ".gitignore contains a negation pattern for .spacelr/ \u2014 refusing to write credentials safety entry. Remove the negation or store credentials globally."
|
|
67
|
+
);
|
|
68
|
+
return { safe: false };
|
|
69
|
+
}
|
|
70
|
+
if (lines.some(matchesExisting)) return { safe: true };
|
|
71
|
+
const needsLeadingNewline = existed && content.length > 0 && !content.endsWith("\n");
|
|
72
|
+
const newContent = (existed ? content : "") + (needsLeadingNewline ? "\n" : "") + entry + "\n";
|
|
73
|
+
try {
|
|
74
|
+
writeFileAtomic(gitignorePath, newContent, 420);
|
|
75
|
+
console.error(`Added ${entry} to .gitignore (credentials must never be committed)`);
|
|
76
|
+
return { safe: true };
|
|
77
|
+
} catch {
|
|
78
|
+
return { safe: true };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function readCredentialsFromDisk(filePath) {
|
|
17
82
|
try {
|
|
18
|
-
const
|
|
19
|
-
if (!fs.existsSync(file)) return null;
|
|
20
|
-
const content = fs.readFileSync(file, "utf-8");
|
|
83
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
21
84
|
const parsed = JSON.parse(content);
|
|
22
|
-
if (typeof parsed !== "object" || parsed === null
|
|
85
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
86
|
+
const obj = parsed;
|
|
87
|
+
if (typeof obj["accessToken"] !== "string" || obj["accessToken"].length === 0 || !Number.isFinite(obj["expiresAt"]) || typeof obj["apiUrl"] !== "string" || obj["apiUrl"].length === 0) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if ("refreshToken" in obj && typeof obj["refreshToken"] !== "string") {
|
|
23
91
|
return null;
|
|
24
92
|
}
|
|
25
93
|
return parsed;
|
|
@@ -27,15 +95,72 @@ function readStoredCredentials() {
|
|
|
27
95
|
return null;
|
|
28
96
|
}
|
|
29
97
|
}
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
98
|
+
function readStoredCredentials() {
|
|
99
|
+
return readStoredCredentialsWithLocation()?.credentials ?? null;
|
|
100
|
+
}
|
|
101
|
+
function readStoredCredentialsWithLocation() {
|
|
102
|
+
const primary = resolveCredentialsFile();
|
|
103
|
+
const fromPrimary = readCredentialsFromDisk(primary.path);
|
|
104
|
+
if (fromPrimary) return { credentials: fromPrimary, location: primary };
|
|
105
|
+
if (primary.scope === "project") {
|
|
106
|
+
const globalPath = getGlobalCredentialsFile();
|
|
107
|
+
const fromGlobal = readCredentialsFromDisk(globalPath);
|
|
108
|
+
if (fromGlobal) {
|
|
109
|
+
return {
|
|
110
|
+
credentials: fromGlobal,
|
|
111
|
+
location: { path: globalPath, scope: "global" }
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
function writeFileAtomic(filePath, data, mode) {
|
|
118
|
+
const tmpPath = `${filePath}.tmp.${process.pid}.${crypto.randomBytes(6).toString("hex")}`;
|
|
119
|
+
try {
|
|
120
|
+
fs.writeFileSync(tmpPath, data, { mode });
|
|
121
|
+
fs.renameSync(tmpPath, filePath);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
try {
|
|
124
|
+
fs.unlinkSync(tmpPath);
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
throw err;
|
|
128
|
+
}
|
|
36
129
|
}
|
|
37
|
-
|
|
38
|
-
if (
|
|
130
|
+
function storeCredentials(credentials, location) {
|
|
131
|
+
if (location.scope === "project" && location.projectDir) {
|
|
132
|
+
const { safe } = ensureGitignored(location.projectDir);
|
|
133
|
+
if (!safe) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"Refusing to write project-local credentials: .gitignore contains a negation pattern that would un-ignore .spacelr/."
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const dir = path.dirname(location.path);
|
|
140
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
141
|
+
try {
|
|
142
|
+
fs.chmodSync(dir, 448);
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
writeFileAtomic(location.path, JSON.stringify(credentials, null, 2), 384);
|
|
146
|
+
}
|
|
147
|
+
function isTokenStillValid(credentials) {
|
|
148
|
+
return Date.now() < credentials.expiresAt - 6e4;
|
|
149
|
+
}
|
|
150
|
+
async function refreshToken(credentials, sourceLocation) {
|
|
151
|
+
if (!credentials.refreshToken) return null;
|
|
152
|
+
try {
|
|
153
|
+
const parsed = new URL(credentials.apiUrl);
|
|
154
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
155
|
+
console.error(
|
|
156
|
+
`Refusing to refresh against non-http(s) URL: ${parsed.protocol}`
|
|
157
|
+
);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
console.error("Refusing to refresh: stored apiUrl is not a valid URL");
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
39
164
|
const apiUrl = credentials.apiUrl.replace(/\/+$/, "");
|
|
40
165
|
const controller = new AbortController();
|
|
41
166
|
const timer = setTimeout(() => controller.abort(), REFRESH_TIMEOUT_MS);
|
|
@@ -49,6 +174,13 @@ async function refreshToken(credentials) {
|
|
|
49
174
|
});
|
|
50
175
|
if (!response.ok) {
|
|
51
176
|
console.error(`Token refresh failed (HTTP ${response.status})`);
|
|
177
|
+
if (response.status === 401) {
|
|
178
|
+
const fresh = readCredentialsFromDisk(sourceLocation.path);
|
|
179
|
+
if (fresh && (fresh.accessToken !== credentials.accessToken || fresh.refreshToken !== credentials.refreshToken) && isTokenStillValid(fresh)) {
|
|
180
|
+
console.error("Using credentials refreshed by concurrent process");
|
|
181
|
+
return fresh.accessToken;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
52
184
|
return null;
|
|
53
185
|
}
|
|
54
186
|
const data = await response.json();
|
|
@@ -56,13 +188,15 @@ async function refreshToken(credentials) {
|
|
|
56
188
|
console.error("Token refresh returned unexpected response shape");
|
|
57
189
|
return null;
|
|
58
190
|
}
|
|
191
|
+
const expiresInRaw = data.expires_in;
|
|
192
|
+
const expiresIn = typeof expiresInRaw === "number" && Number.isFinite(expiresInRaw) && expiresInRaw > 0 ? expiresInRaw : 3600;
|
|
59
193
|
const updated = {
|
|
60
194
|
accessToken: data.access_token,
|
|
61
195
|
refreshToken: data.refresh_token ?? credentials.refreshToken,
|
|
62
|
-
expiresAt: Date.now() +
|
|
196
|
+
expiresAt: Date.now() + expiresIn * 1e3,
|
|
63
197
|
apiUrl: credentials.apiUrl
|
|
64
198
|
};
|
|
65
|
-
storeCredentials(updated);
|
|
199
|
+
storeCredentials(updated, sourceLocation);
|
|
66
200
|
console.error("Token refreshed successfully");
|
|
67
201
|
return data.access_token;
|
|
68
202
|
} catch (err) {
|
|
@@ -79,14 +213,19 @@ async function refreshToken(credentials) {
|
|
|
79
213
|
async function resolveAuthToken(forceRefresh = false) {
|
|
80
214
|
const envToken = process.env["SPACELR_AUTH_TOKEN"];
|
|
81
215
|
if (envToken) return envToken;
|
|
82
|
-
const
|
|
83
|
-
if (!
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
216
|
+
const loaded = readStoredCredentialsWithLocation();
|
|
217
|
+
if (!loaded) return null;
|
|
218
|
+
return resolveAuthTokenFromLoaded(loaded, forceRefresh);
|
|
219
|
+
}
|
|
220
|
+
async function resolveAuthTokenFromLoaded(loaded, forceRefresh) {
|
|
221
|
+
const { credentials, location } = loaded;
|
|
222
|
+
if (!forceRefresh && isTokenStillValid(credentials)) {
|
|
223
|
+
return credentials.accessToken;
|
|
224
|
+
}
|
|
225
|
+
return refreshToken(credentials, location);
|
|
87
226
|
}
|
|
88
227
|
function warnIfInsecureUrl(url) {
|
|
89
|
-
if (url.startsWith("http://") && !url.includes("localhost") && !/^http:\/\/(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|
|
|
228
|
+
if (url.startsWith("http://") && !url.includes("localhost") && !/^http:\/\/(127\.|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|(\[::1\])|::1)(\/|:|$)/.test(url)) {
|
|
90
229
|
console.error(`Warning: API URL uses plain HTTP (${url}). Credentials will be transmitted unencrypted.`);
|
|
91
230
|
}
|
|
92
231
|
}
|
|
@@ -123,20 +262,24 @@ function resolveApiBaseUrl() {
|
|
|
123
262
|
try {
|
|
124
263
|
const credentials = readStoredCredentials();
|
|
125
264
|
return resolveApiUrl(credentials);
|
|
126
|
-
} catch {
|
|
265
|
+
} catch (err) {
|
|
266
|
+
console.error(
|
|
267
|
+
`Failed to resolve admin API URL: ${err instanceof Error ? err.message : String(err)}`
|
|
268
|
+
);
|
|
127
269
|
return null;
|
|
128
270
|
}
|
|
129
271
|
}
|
|
130
272
|
async function loadConfig() {
|
|
131
|
-
const
|
|
132
|
-
const
|
|
273
|
+
const envToken = process.env["SPACELR_AUTH_TOKEN"];
|
|
274
|
+
const loaded = readStoredCredentialsWithLocation();
|
|
275
|
+
const authToken = envToken ?? (loaded ? await resolveAuthTokenFromLoaded(loaded, false) : null);
|
|
133
276
|
if (!authToken) {
|
|
134
277
|
throw new Error(
|
|
135
278
|
'No auth token found. Either set SPACELR_AUTH_TOKEN or run "spacelr login" first.'
|
|
136
279
|
);
|
|
137
280
|
}
|
|
138
281
|
return {
|
|
139
|
-
apiBaseUrl: resolveApiUrl(credentials),
|
|
282
|
+
apiBaseUrl: resolveApiUrl(loaded?.credentials ?? null),
|
|
140
283
|
authToken,
|
|
141
284
|
clientId: process.env["SPACELR_CLIENT_ID"],
|
|
142
285
|
projectId: process.env["SPACELR_PROJECT_ID"]
|
|
@@ -145,6 +288,7 @@ async function loadConfig() {
|
|
|
145
288
|
|
|
146
289
|
// libs/mcp-server/src/api-client.ts
|
|
147
290
|
var REQUEST_TIMEOUT_MS = 3e4;
|
|
291
|
+
var UPLOAD_TIMEOUT_MS = 12e4;
|
|
148
292
|
var ApiError = class extends Error {
|
|
149
293
|
constructor(status, statusText, responseBody) {
|
|
150
294
|
const truncated = responseBody.length > 500 ? responseBody.slice(0, 500) + "..." : responseBody;
|
|
@@ -183,6 +327,77 @@ var ApiClient = class {
|
|
|
183
327
|
async delete(path2, opts) {
|
|
184
328
|
return this.requestWithRetry("DELETE", path2, opts);
|
|
185
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Upload a file as multipart/form-data.
|
|
332
|
+
* Includes the same 401-retry logic as regular requests.
|
|
333
|
+
*
|
|
334
|
+
* @param path - API path (e.g. `/projects/:id/functions/:id/deploy`)
|
|
335
|
+
* @param buffer - Raw file content
|
|
336
|
+
* @param filename - Name sent in the Content-Disposition header (e.g. "bundle.zip")
|
|
337
|
+
*/
|
|
338
|
+
async uploadFile(path2, buffer, filename) {
|
|
339
|
+
try {
|
|
340
|
+
return await this.sendMultipart(path2, buffer, filename);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
if (error instanceof ApiError && error.status === 401) {
|
|
343
|
+
const currentToken = this.headers["Authorization"];
|
|
344
|
+
let newToken = await this.refreshAuthToken();
|
|
345
|
+
const newBaseUrl = resolveApiBaseUrl();
|
|
346
|
+
if (newBaseUrl && newBaseUrl !== this.baseUrl) {
|
|
347
|
+
this.baseUrl = newBaseUrl;
|
|
348
|
+
}
|
|
349
|
+
if (newToken && `Bearer ${newToken}` === currentToken) {
|
|
350
|
+
newToken = await resolveAuthToken(true);
|
|
351
|
+
}
|
|
352
|
+
if (newToken && `Bearer ${newToken}` !== currentToken) {
|
|
353
|
+
this.headers["Authorization"] = `Bearer ${newToken}`;
|
|
354
|
+
return this.sendMultipart(path2, buffer, filename);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
throw error;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async sendMultipart(path2, buffer, filename) {
|
|
361
|
+
const boundary = `----MCP${Date.now()}${Math.random().toString(36).slice(2)}`;
|
|
362
|
+
const header = Buffer.from(
|
|
363
|
+
`--${boundary}\r
|
|
364
|
+
Content-Disposition: form-data; name="file"; filename="${filename}"\r
|
|
365
|
+
Content-Type: application/zip\r
|
|
366
|
+
\r
|
|
367
|
+
`
|
|
368
|
+
);
|
|
369
|
+
const footer = Buffer.from(`\r
|
|
370
|
+
--${boundary}--\r
|
|
371
|
+
`);
|
|
372
|
+
const body = Buffer.concat([header, buffer, footer]);
|
|
373
|
+
const url = `${this.baseUrl}${path2}`;
|
|
374
|
+
const controller = new AbortController();
|
|
375
|
+
const timer = setTimeout(() => controller.abort(), UPLOAD_TIMEOUT_MS);
|
|
376
|
+
try {
|
|
377
|
+
const response = await fetch(url, {
|
|
378
|
+
method: "POST",
|
|
379
|
+
headers: {
|
|
380
|
+
...this.headers,
|
|
381
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
382
|
+
"Content-Length": String(body.length)
|
|
383
|
+
},
|
|
384
|
+
body,
|
|
385
|
+
signal: controller.signal
|
|
386
|
+
});
|
|
387
|
+
const text = await response.text();
|
|
388
|
+
if (!response.ok) {
|
|
389
|
+
throw new ApiError(response.status, response.statusText, text);
|
|
390
|
+
}
|
|
391
|
+
if (!text) return void 0;
|
|
392
|
+
try {
|
|
393
|
+
return JSON.parse(text);
|
|
394
|
+
} catch {
|
|
395
|
+
return text;
|
|
396
|
+
}
|
|
397
|
+
} finally {
|
|
398
|
+
clearTimeout(timer);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
186
401
|
async refreshAuthToken(force = false) {
|
|
187
402
|
if (!this.refreshPromise) {
|
|
188
403
|
this.refreshPromise = resolveAuthToken(force).finally(() => {
|
|
@@ -1344,6 +1559,104 @@ function registerDatabaseTools(server, api) {
|
|
|
1344
1559
|
|
|
1345
1560
|
// libs/mcp-server/src/tools/hosting.ts
|
|
1346
1561
|
import { z as z5 } from "zod";
|
|
1562
|
+
|
|
1563
|
+
// libs/mcp-server/src/zip.ts
|
|
1564
|
+
import { deflateRawSync } from "zlib";
|
|
1565
|
+
var ZipBuilder = class {
|
|
1566
|
+
constructor() {
|
|
1567
|
+
this.files = [];
|
|
1568
|
+
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Add a text file to the archive.
|
|
1571
|
+
*
|
|
1572
|
+
* @param name - File path inside the ZIP (e.g. "index.js" or "src/utils.ts")
|
|
1573
|
+
* @param content - UTF-8 text content of the file
|
|
1574
|
+
*/
|
|
1575
|
+
addFile(name, content) {
|
|
1576
|
+
const nameBuffer = Buffer.from(name, "utf-8");
|
|
1577
|
+
const contentBuffer = Buffer.from(content, "utf-8");
|
|
1578
|
+
const compressed = deflateRawSync(contentBuffer, { level: 9 });
|
|
1579
|
+
const crc32 = computeCrc32(contentBuffer);
|
|
1580
|
+
this.files.push({
|
|
1581
|
+
name: nameBuffer,
|
|
1582
|
+
content: contentBuffer,
|
|
1583
|
+
compressed,
|
|
1584
|
+
crc32
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
/** Generate the complete ZIP archive as a Buffer. */
|
|
1588
|
+
toBuffer() {
|
|
1589
|
+
const localHeaders = [];
|
|
1590
|
+
const centralHeaders = [];
|
|
1591
|
+
let offset = 0;
|
|
1592
|
+
for (const file of this.files) {
|
|
1593
|
+
const local = Buffer.alloc(30);
|
|
1594
|
+
local.writeUInt32LE(67324752, 0);
|
|
1595
|
+
local.writeUInt16LE(20, 4);
|
|
1596
|
+
local.writeUInt16LE(0, 6);
|
|
1597
|
+
local.writeUInt16LE(8, 8);
|
|
1598
|
+
local.writeUInt16LE(0, 10);
|
|
1599
|
+
local.writeUInt16LE(0, 12);
|
|
1600
|
+
local.writeUInt32LE(file.crc32, 14);
|
|
1601
|
+
local.writeUInt32LE(file.compressed.length, 18);
|
|
1602
|
+
local.writeUInt32LE(file.content.length, 22);
|
|
1603
|
+
local.writeUInt16LE(file.name.length, 26);
|
|
1604
|
+
local.writeUInt16LE(0, 28);
|
|
1605
|
+
localHeaders.push(local, file.name, file.compressed);
|
|
1606
|
+
const central = Buffer.alloc(46);
|
|
1607
|
+
central.writeUInt32LE(33639248, 0);
|
|
1608
|
+
central.writeUInt16LE(20, 4);
|
|
1609
|
+
central.writeUInt16LE(20, 6);
|
|
1610
|
+
central.writeUInt16LE(0, 8);
|
|
1611
|
+
central.writeUInt16LE(8, 10);
|
|
1612
|
+
central.writeUInt16LE(0, 12);
|
|
1613
|
+
central.writeUInt16LE(0, 14);
|
|
1614
|
+
central.writeUInt32LE(file.crc32, 16);
|
|
1615
|
+
central.writeUInt32LE(file.compressed.length, 20);
|
|
1616
|
+
central.writeUInt32LE(file.content.length, 24);
|
|
1617
|
+
central.writeUInt16LE(file.name.length, 28);
|
|
1618
|
+
central.writeUInt16LE(0, 30);
|
|
1619
|
+
central.writeUInt16LE(0, 32);
|
|
1620
|
+
central.writeUInt16LE(0, 34);
|
|
1621
|
+
central.writeUInt16LE(0, 36);
|
|
1622
|
+
central.writeUInt32LE(0, 38);
|
|
1623
|
+
central.writeUInt32LE(offset, 42);
|
|
1624
|
+
centralHeaders.push(central, file.name);
|
|
1625
|
+
offset += 30 + file.name.length + file.compressed.length;
|
|
1626
|
+
}
|
|
1627
|
+
const centralDirSize = centralHeaders.reduce((sum, b) => sum + b.length, 0);
|
|
1628
|
+
const eocd = Buffer.alloc(22);
|
|
1629
|
+
eocd.writeUInt32LE(101010256, 0);
|
|
1630
|
+
eocd.writeUInt16LE(0, 4);
|
|
1631
|
+
eocd.writeUInt16LE(0, 6);
|
|
1632
|
+
eocd.writeUInt16LE(this.files.length, 8);
|
|
1633
|
+
eocd.writeUInt16LE(this.files.length, 10);
|
|
1634
|
+
eocd.writeUInt32LE(centralDirSize, 12);
|
|
1635
|
+
eocd.writeUInt32LE(offset, 16);
|
|
1636
|
+
eocd.writeUInt16LE(0, 20);
|
|
1637
|
+
return Buffer.concat([...localHeaders, ...centralHeaders, eocd]);
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
var crcTable = (() => {
|
|
1641
|
+
const table = new Uint32Array(256);
|
|
1642
|
+
for (let n = 0; n < 256; n++) {
|
|
1643
|
+
let c = n;
|
|
1644
|
+
for (let k = 0; k < 8; k++) {
|
|
1645
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
1646
|
+
}
|
|
1647
|
+
table[n] = c;
|
|
1648
|
+
}
|
|
1649
|
+
return table;
|
|
1650
|
+
})();
|
|
1651
|
+
function computeCrc32(data) {
|
|
1652
|
+
let crc = 4294967295;
|
|
1653
|
+
for (let i = 0; i < data.length; i++) {
|
|
1654
|
+
crc = crcTable[(crc ^ data[i]) & 255] ^ crc >>> 8;
|
|
1655
|
+
}
|
|
1656
|
+
return (crc ^ 4294967295) >>> 0;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// libs/mcp-server/src/tools/hosting.ts
|
|
1347
1660
|
function registerHostingTools(server, api) {
|
|
1348
1661
|
server.registerTool(
|
|
1349
1662
|
"hosting_deployments_list",
|
|
@@ -1424,6 +1737,57 @@ function registerHostingTools(server, api) {
|
|
|
1424
1737
|
}
|
|
1425
1738
|
}
|
|
1426
1739
|
);
|
|
1740
|
+
server.registerTool(
|
|
1741
|
+
"hosting_deployments_upload",
|
|
1742
|
+
{
|
|
1743
|
+
description: 'Upload files to an existing hosting deployment. Accepts a record of filename \u2192 content, bundles them into a ZIP archive, and uploads it.\n\nTypical workflow:\n 1. hosting_deployments_create \u2192 get deploymentId\n 2. hosting_deployments_upload \u2192 upload site files\n 3. hosting_deployments_activate \u2192 make the deployment live\n\nExample:\n files: { "index.html": "<!DOCTYPE html>...", "style.css": "body { ... }", "app.js": "..." }',
|
|
1744
|
+
inputSchema: {
|
|
1745
|
+
projectId: z5.string().describe("Project ID"),
|
|
1746
|
+
deploymentId: z5.string().describe("Deployment ID (from hosting_deployments_create)"),
|
|
1747
|
+
files: z5.record(z5.string(), z5.string()).describe(
|
|
1748
|
+
'Map of filename \u2192 file content. Keys are file paths inside the archive (e.g. "index.html", "assets/style.css"). Values are the UTF-8 file contents.'
|
|
1749
|
+
)
|
|
1750
|
+
}
|
|
1751
|
+
},
|
|
1752
|
+
async ({ projectId, deploymentId, files }) => {
|
|
1753
|
+
try {
|
|
1754
|
+
const fileEntries = Object.entries(files);
|
|
1755
|
+
if (fileEntries.length === 0) {
|
|
1756
|
+
return {
|
|
1757
|
+
content: [{ type: "text", text: "files must contain at least one entry" }],
|
|
1758
|
+
isError: true
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
const zip = new ZipBuilder();
|
|
1762
|
+
for (const [name, content] of fileEntries) {
|
|
1763
|
+
zip.addFile(name, content);
|
|
1764
|
+
}
|
|
1765
|
+
const zipBuffer = zip.toBuffer();
|
|
1766
|
+
const result = await api.uploadFile(
|
|
1767
|
+
`/hosting/projects/${encodeURIComponent(projectId)}/deployments/${encodeURIComponent(deploymentId)}/upload`,
|
|
1768
|
+
zipBuffer,
|
|
1769
|
+
"site.zip"
|
|
1770
|
+
);
|
|
1771
|
+
const response = {
|
|
1772
|
+
success: true,
|
|
1773
|
+
filesUploaded: fileEntries.map(([name]) => name),
|
|
1774
|
+
bundleSizeBytes: zipBuffer.length
|
|
1775
|
+
};
|
|
1776
|
+
if (result && typeof result === "object") {
|
|
1777
|
+
Object.assign(response, result);
|
|
1778
|
+
}
|
|
1779
|
+
return {
|
|
1780
|
+
content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
|
|
1781
|
+
};
|
|
1782
|
+
} catch (error) {
|
|
1783
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1784
|
+
return {
|
|
1785
|
+
content: [{ type: "text", text: `Failed to upload deployment: ${message}` }],
|
|
1786
|
+
isError: true
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
);
|
|
1427
1791
|
server.registerTool(
|
|
1428
1792
|
"hosting_deployments_activate",
|
|
1429
1793
|
{
|
|
@@ -2138,6 +2502,70 @@ function registerFunctionTools(server, api) {
|
|
|
2138
2502
|
}
|
|
2139
2503
|
}
|
|
2140
2504
|
);
|
|
2505
|
+
server.registerTool(
|
|
2506
|
+
"functions_deploy",
|
|
2507
|
+
{
|
|
2508
|
+
description: `Deploy code to a serverless function. Accepts one or more files as a record of filename \u2192 content. The files are bundled into a ZIP archive and uploaded. Use this after creating a function with functions_create.
|
|
2509
|
+
|
|
2510
|
+
Example \u2013 single file:
|
|
2511
|
+
files: { "index.js": "export default async function handler(ctx) { ... }" }
|
|
2512
|
+
|
|
2513
|
+
Example \u2013 multiple files:
|
|
2514
|
+
files: { "index.js": "import {hello} from './lib.js'; ...", "lib.js": "export const hello = () => ..." }`,
|
|
2515
|
+
inputSchema: {
|
|
2516
|
+
projectId: z7.string().describe("Project ID"),
|
|
2517
|
+
functionId: z7.string().describe("Function ID (from functions_create or functions_list)"),
|
|
2518
|
+
files: z7.record(z7.string(), z7.string()).describe(
|
|
2519
|
+
'Map of filename \u2192 file content. Keys are file paths inside the bundle (e.g. "index.js", "src/helper.ts"). Values are the UTF-8 source code.'
|
|
2520
|
+
),
|
|
2521
|
+
entryPoint: z7.string().max(255).optional().describe('Entry point file inside the bundle (default: "index.js"). Must match a key in files.')
|
|
2522
|
+
}
|
|
2523
|
+
},
|
|
2524
|
+
async ({ projectId, functionId, files, entryPoint }) => {
|
|
2525
|
+
try {
|
|
2526
|
+
const fileEntries = Object.entries(files);
|
|
2527
|
+
if (fileEntries.length === 0) {
|
|
2528
|
+
return {
|
|
2529
|
+
content: [{ type: "text", text: "files must contain at least one entry" }],
|
|
2530
|
+
isError: true
|
|
2531
|
+
};
|
|
2532
|
+
}
|
|
2533
|
+
if (entryPoint) {
|
|
2534
|
+
await api.patch(
|
|
2535
|
+
`/projects/${encodeURIComponent(projectId)}/functions/${encodeURIComponent(functionId)}`,
|
|
2536
|
+
{ body: { entryPoint } }
|
|
2537
|
+
);
|
|
2538
|
+
}
|
|
2539
|
+
const zip = new ZipBuilder();
|
|
2540
|
+
for (const [name, content] of fileEntries) {
|
|
2541
|
+
zip.addFile(name, content);
|
|
2542
|
+
}
|
|
2543
|
+
const zipBuffer = zip.toBuffer();
|
|
2544
|
+
const result = await api.uploadFile(
|
|
2545
|
+
`/projects/${encodeURIComponent(projectId)}/functions/${encodeURIComponent(functionId)}/deploy`,
|
|
2546
|
+
zipBuffer,
|
|
2547
|
+
"bundle.zip"
|
|
2548
|
+
);
|
|
2549
|
+
const response = {
|
|
2550
|
+
success: true,
|
|
2551
|
+
filesDeployed: fileEntries.map(([name]) => name),
|
|
2552
|
+
bundleSizeBytes: zipBuffer.length
|
|
2553
|
+
};
|
|
2554
|
+
if (result && typeof result === "object") {
|
|
2555
|
+
Object.assign(response, result);
|
|
2556
|
+
}
|
|
2557
|
+
return {
|
|
2558
|
+
content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
|
|
2559
|
+
};
|
|
2560
|
+
} catch (error) {
|
|
2561
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2562
|
+
return {
|
|
2563
|
+
content: [{ type: "text", text: `Failed to deploy function: ${message}` }],
|
|
2564
|
+
isError: true
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
);
|
|
2141
2569
|
server.registerTool(
|
|
2142
2570
|
"functions_deployments_list",
|
|
2143
2571
|
{
|
|
@@ -2555,104 +2983,1059 @@ function registerEmailTemplateTools(server, api) {
|
|
|
2555
2983
|
);
|
|
2556
2984
|
}
|
|
2557
2985
|
|
|
2558
|
-
// libs/mcp-server/src/tools/
|
|
2559
|
-
function registerAllTools(server, api) {
|
|
2560
|
-
registerAuthTools(server, api);
|
|
2561
|
-
registerProjectTools(server, api);
|
|
2562
|
-
registerClientTools(server, api);
|
|
2563
|
-
registerDatabaseTools(server, api);
|
|
2564
|
-
registerHostingTools(server, api);
|
|
2565
|
-
registerStorageTools(server, api);
|
|
2566
|
-
registerFunctionTools(server, api);
|
|
2567
|
-
registerEmailTemplateTools(server, api);
|
|
2568
|
-
}
|
|
2569
|
-
|
|
2570
|
-
// libs/mcp-server/src/prompts/database.ts
|
|
2986
|
+
// libs/mcp-server/src/tools/cronJobs.ts
|
|
2571
2987
|
import { z as z9 } from "zod";
|
|
2572
|
-
function
|
|
2573
|
-
server.
|
|
2574
|
-
"
|
|
2988
|
+
function registerCronJobTools(server, api) {
|
|
2989
|
+
server.registerTool(
|
|
2990
|
+
"cron_jobs_list",
|
|
2575
2991
|
{
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
projectId: z9.string().describe("Project ID"),
|
|
2580
|
-
collectionName: z9.string().describe("Name of the collection to create"),
|
|
2581
|
-
access: z9.enum(["public-read", "authenticated", "admin-only"]).optional().describe("Access level (default: admin-only)")
|
|
2992
|
+
description: "List all cron jobs for a project",
|
|
2993
|
+
inputSchema: {
|
|
2994
|
+
projectId: z9.string()
|
|
2582
2995
|
}
|
|
2583
2996
|
},
|
|
2584
|
-
async ({ projectId
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
".delete": "auth.role === 'owner' || auth.role === 'admin'"
|
|
2598
|
-
},
|
|
2599
|
-
"admin-only": {
|
|
2600
|
-
".create": "auth.role === 'owner' || auth.role === 'admin'",
|
|
2601
|
-
".read": "auth.role === 'owner' || auth.role === 'admin'",
|
|
2602
|
-
".update": "auth.role === 'owner' || auth.role === 'admin'",
|
|
2603
|
-
".delete": "auth.role === 'owner' || auth.role === 'admin'"
|
|
2604
|
-
}
|
|
2605
|
-
};
|
|
2606
|
-
const collectionRules = rules[accessLevel];
|
|
2607
|
-
const rulesJson = JSON.stringify(collectionRules, null, 2);
|
|
2608
|
-
return {
|
|
2609
|
-
messages: [
|
|
2610
|
-
{
|
|
2611
|
-
role: "user",
|
|
2612
|
-
content: {
|
|
2613
|
-
type: "text",
|
|
2614
|
-
text: [
|
|
2615
|
-
`Set up a new database collection "${collectionName}" in project ${projectId}.`,
|
|
2616
|
-
"",
|
|
2617
|
-
"IMPORTANT WORKFLOW:",
|
|
2618
|
-
"1. First, get the current rules with database_rules_get",
|
|
2619
|
-
"2. Add the new collection rules to the existing rules object",
|
|
2620
|
-
"3. Set the updated rules with database_rules_set \u2014 this automatically creates the collection",
|
|
2621
|
-
'4. Do NOT try to insert documents before the rules are set \u2014 it will fail with "Rule denied"',
|
|
2622
|
-
"",
|
|
2623
|
-
`Suggested rules for "${collectionName}" (${accessLevel}):`,
|
|
2624
|
-
rulesJson,
|
|
2625
|
-
"",
|
|
2626
|
-
"The $other catch-all rule should deny access to undefined collections:",
|
|
2627
|
-
' "$other": { ".read": "false", ".write": "false" }',
|
|
2628
|
-
"",
|
|
2629
|
-
"After rules are set, the collection is ready for use via:",
|
|
2630
|
-
"- database_documents_insert",
|
|
2631
|
-
"- database_documents_find",
|
|
2632
|
-
'- Serverless functions using spacelr.db.collection("' + collectionName + '")'
|
|
2633
|
-
].join("\n")
|
|
2634
|
-
}
|
|
2635
|
-
}
|
|
2636
|
-
]
|
|
2637
|
-
};
|
|
2997
|
+
async ({ projectId }) => {
|
|
2998
|
+
try {
|
|
2999
|
+
const result = await api.get(`/projects/${encodeURIComponent(projectId)}/cron-jobs`);
|
|
3000
|
+
return {
|
|
3001
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3002
|
+
};
|
|
3003
|
+
} catch (error) {
|
|
3004
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3005
|
+
return {
|
|
3006
|
+
content: [{ type: "text", text: `Failed to list cron jobs: ${message}` }],
|
|
3007
|
+
isError: true
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
2638
3010
|
}
|
|
2639
3011
|
);
|
|
2640
|
-
server.
|
|
2641
|
-
"
|
|
3012
|
+
server.registerTool(
|
|
3013
|
+
"cron_jobs_get",
|
|
2642
3014
|
{
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
3015
|
+
description: "Get a single cron job by ID",
|
|
3016
|
+
inputSchema: {
|
|
3017
|
+
projectId: z9.string(),
|
|
3018
|
+
jobId: z9.string()
|
|
2647
3019
|
}
|
|
2648
3020
|
},
|
|
2649
|
-
async ({ projectId }) => {
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
3021
|
+
async ({ projectId, jobId }) => {
|
|
3022
|
+
try {
|
|
3023
|
+
const result = await api.get(`/projects/${encodeURIComponent(projectId)}/cron-jobs/${encodeURIComponent(jobId)}`);
|
|
3024
|
+
return {
|
|
3025
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3026
|
+
};
|
|
3027
|
+
} catch (error) {
|
|
3028
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3029
|
+
return {
|
|
3030
|
+
content: [{ type: "text", text: `Failed to get cron job: ${message}` }],
|
|
3031
|
+
isError: true
|
|
3032
|
+
};
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
);
|
|
3036
|
+
server.registerTool(
|
|
3037
|
+
"cron_jobs_create",
|
|
3038
|
+
{
|
|
3039
|
+
description: 'Create a new cron job. When type is "webhook", url is required. When type is "function", functionId is required. cronExpression uses 5-field cron format (e.g. "0 0 * * *").',
|
|
3040
|
+
inputSchema: {
|
|
3041
|
+
projectId: z9.string(),
|
|
3042
|
+
name: z9.string(),
|
|
3043
|
+
cronExpression: z9.string().regex(/^(\S+\s){4}\S+$/),
|
|
3044
|
+
timezone: z9.string().optional(),
|
|
3045
|
+
type: z9.enum(["webhook", "function"]).optional(),
|
|
3046
|
+
functionId: z9.string().optional(),
|
|
3047
|
+
url: z9.string().url().optional(),
|
|
3048
|
+
secret: z9.string().optional(),
|
|
3049
|
+
payload: z9.record(z9.string(), z9.unknown()).optional(),
|
|
3050
|
+
retryAttempts: z9.number().int().min(0).max(10).optional(),
|
|
3051
|
+
timeout: z9.number().int().min(1e3).max(12e4).optional(),
|
|
3052
|
+
enabled: z9.boolean().optional()
|
|
3053
|
+
}
|
|
3054
|
+
},
|
|
3055
|
+
async ({ projectId, name, cronExpression, timezone, type, functionId, url, secret, payload, retryAttempts, timeout, enabled }) => {
|
|
3056
|
+
try {
|
|
3057
|
+
const body = { name, cronExpression };
|
|
3058
|
+
if (timezone !== void 0) body.timezone = timezone;
|
|
3059
|
+
if (type !== void 0) body.type = type;
|
|
3060
|
+
if (functionId !== void 0) body.functionId = functionId;
|
|
3061
|
+
if (url !== void 0) body.url = url;
|
|
3062
|
+
if (secret !== void 0) body.secret = secret;
|
|
3063
|
+
if (payload !== void 0) body.payload = payload;
|
|
3064
|
+
if (retryAttempts !== void 0) body.retryAttempts = retryAttempts;
|
|
3065
|
+
if (timeout !== void 0) body.timeout = timeout;
|
|
3066
|
+
if (enabled !== void 0) body.enabled = enabled;
|
|
3067
|
+
const result = await api.post(`/projects/${encodeURIComponent(projectId)}/cron-jobs`, { body });
|
|
3068
|
+
return {
|
|
3069
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3070
|
+
};
|
|
3071
|
+
} catch (error) {
|
|
3072
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3073
|
+
return {
|
|
3074
|
+
content: [{ type: "text", text: `Failed to create cron job: ${message}` }],
|
|
3075
|
+
isError: true
|
|
3076
|
+
};
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
);
|
|
3080
|
+
server.registerTool(
|
|
3081
|
+
"cron_jobs_update",
|
|
3082
|
+
{
|
|
3083
|
+
description: "Update an existing cron job",
|
|
3084
|
+
inputSchema: {
|
|
3085
|
+
projectId: z9.string(),
|
|
3086
|
+
jobId: z9.string(),
|
|
3087
|
+
name: z9.string().optional(),
|
|
3088
|
+
cronExpression: z9.string().regex(/^(\S+\s){4}\S+$/).optional(),
|
|
3089
|
+
timezone: z9.string().optional(),
|
|
3090
|
+
type: z9.enum(["webhook", "function"]).optional(),
|
|
3091
|
+
functionId: z9.string().optional(),
|
|
3092
|
+
url: z9.string().url().optional(),
|
|
3093
|
+
secret: z9.string().optional(),
|
|
3094
|
+
payload: z9.record(z9.string(), z9.unknown()).optional(),
|
|
3095
|
+
retryAttempts: z9.number().int().min(0).max(10).optional(),
|
|
3096
|
+
timeout: z9.number().int().min(1e3).max(12e4).optional(),
|
|
3097
|
+
enabled: z9.boolean().optional()
|
|
3098
|
+
}
|
|
3099
|
+
},
|
|
3100
|
+
async ({ projectId, jobId, name, cronExpression, timezone, type, functionId, url, secret, payload, retryAttempts, timeout, enabled }) => {
|
|
3101
|
+
try {
|
|
3102
|
+
const body = {};
|
|
3103
|
+
if (name !== void 0) body.name = name;
|
|
3104
|
+
if (cronExpression !== void 0) body.cronExpression = cronExpression;
|
|
3105
|
+
if (timezone !== void 0) body.timezone = timezone;
|
|
3106
|
+
if (type !== void 0) body.type = type;
|
|
3107
|
+
if (functionId !== void 0) body.functionId = functionId;
|
|
3108
|
+
if (url !== void 0) body.url = url;
|
|
3109
|
+
if (secret !== void 0) body.secret = secret;
|
|
3110
|
+
if (payload !== void 0) body.payload = payload;
|
|
3111
|
+
if (retryAttempts !== void 0) body.retryAttempts = retryAttempts;
|
|
3112
|
+
if (timeout !== void 0) body.timeout = timeout;
|
|
3113
|
+
if (enabled !== void 0) body.enabled = enabled;
|
|
3114
|
+
if (Object.keys(body).length === 0) {
|
|
3115
|
+
return {
|
|
3116
|
+
content: [{ type: "text", text: "At least one field must be provided" }],
|
|
3117
|
+
isError: true
|
|
3118
|
+
};
|
|
3119
|
+
}
|
|
3120
|
+
const result = await api.patch(`/projects/${encodeURIComponent(projectId)}/cron-jobs/${encodeURIComponent(jobId)}`, { body });
|
|
3121
|
+
return {
|
|
3122
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3123
|
+
};
|
|
3124
|
+
} catch (error) {
|
|
3125
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3126
|
+
return {
|
|
3127
|
+
content: [{ type: "text", text: `Failed to update cron job: ${message}` }],
|
|
3128
|
+
isError: true
|
|
3129
|
+
};
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
);
|
|
3133
|
+
server.registerTool(
|
|
3134
|
+
"cron_jobs_delete",
|
|
3135
|
+
{
|
|
3136
|
+
description: "Delete a cron job",
|
|
3137
|
+
inputSchema: {
|
|
3138
|
+
projectId: z9.string(),
|
|
3139
|
+
jobId: z9.string()
|
|
3140
|
+
}
|
|
3141
|
+
},
|
|
3142
|
+
async ({ projectId, jobId }) => {
|
|
3143
|
+
try {
|
|
3144
|
+
const result = await api.delete(`/projects/${encodeURIComponent(projectId)}/cron-jobs/${encodeURIComponent(jobId)}`);
|
|
3145
|
+
return {
|
|
3146
|
+
content: [{ type: "text", text: result ? JSON.stringify(result, null, 2) : "Cron job deleted" }]
|
|
3147
|
+
};
|
|
3148
|
+
} catch (error) {
|
|
3149
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3150
|
+
return {
|
|
3151
|
+
content: [{ type: "text", text: `Failed to delete cron job: ${message}` }],
|
|
3152
|
+
isError: true
|
|
3153
|
+
};
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
);
|
|
3157
|
+
server.registerTool(
|
|
3158
|
+
"cron_jobs_trigger",
|
|
3159
|
+
{
|
|
3160
|
+
description: "Manually trigger a cron job execution",
|
|
3161
|
+
inputSchema: {
|
|
3162
|
+
projectId: z9.string(),
|
|
3163
|
+
jobId: z9.string()
|
|
3164
|
+
}
|
|
3165
|
+
},
|
|
3166
|
+
async ({ projectId, jobId }) => {
|
|
3167
|
+
try {
|
|
3168
|
+
const result = await api.post(`/projects/${encodeURIComponent(projectId)}/cron-jobs/${encodeURIComponent(jobId)}/trigger`);
|
|
3169
|
+
return {
|
|
3170
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3171
|
+
};
|
|
3172
|
+
} catch (error) {
|
|
3173
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3174
|
+
return {
|
|
3175
|
+
content: [{ type: "text", text: `Failed to trigger cron job: ${message}` }],
|
|
3176
|
+
isError: true
|
|
3177
|
+
};
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
);
|
|
3181
|
+
server.registerTool(
|
|
3182
|
+
"cron_jobs_toggle",
|
|
3183
|
+
{
|
|
3184
|
+
description: "Toggle a cron job enabled/disabled",
|
|
3185
|
+
inputSchema: {
|
|
3186
|
+
projectId: z9.string(),
|
|
3187
|
+
jobId: z9.string(),
|
|
3188
|
+
enabled: z9.boolean()
|
|
3189
|
+
}
|
|
3190
|
+
},
|
|
3191
|
+
async ({ projectId, jobId, enabled }) => {
|
|
3192
|
+
try {
|
|
3193
|
+
const result = await api.post(`/projects/${encodeURIComponent(projectId)}/cron-jobs/${encodeURIComponent(jobId)}/toggle`, { body: { enabled } });
|
|
3194
|
+
return {
|
|
3195
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3196
|
+
};
|
|
3197
|
+
} catch (error) {
|
|
3198
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3199
|
+
return {
|
|
3200
|
+
content: [{ type: "text", text: `Failed to toggle cron job: ${message}` }],
|
|
3201
|
+
isError: true
|
|
3202
|
+
};
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
);
|
|
3206
|
+
server.registerTool(
|
|
3207
|
+
"cron_jobs_executions",
|
|
3208
|
+
{
|
|
3209
|
+
description: "List execution history for a cron job",
|
|
3210
|
+
inputSchema: {
|
|
3211
|
+
projectId: z9.string(),
|
|
3212
|
+
jobId: z9.string(),
|
|
3213
|
+
limit: z9.number().int().min(1).max(100).optional(),
|
|
3214
|
+
offset: z9.number().int().min(0).optional()
|
|
3215
|
+
}
|
|
3216
|
+
},
|
|
3217
|
+
async ({ projectId, jobId, limit, offset }) => {
|
|
3218
|
+
try {
|
|
3219
|
+
const result = await api.get(`/projects/${encodeURIComponent(projectId)}/cron-jobs/${encodeURIComponent(jobId)}/executions`, { params: { limit, offset } });
|
|
3220
|
+
return {
|
|
3221
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3222
|
+
};
|
|
3223
|
+
} catch (error) {
|
|
3224
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3225
|
+
return {
|
|
3226
|
+
content: [{ type: "text", text: `Failed to list cron job executions: ${message}` }],
|
|
3227
|
+
isError: true
|
|
3228
|
+
};
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
);
|
|
3232
|
+
server.registerTool(
|
|
3233
|
+
"cron_jobs_list_all",
|
|
3234
|
+
{
|
|
3235
|
+
description: "List all cron jobs across projects (admin overview)",
|
|
3236
|
+
inputSchema: {
|
|
3237
|
+
projectId: z9.string().optional()
|
|
3238
|
+
}
|
|
3239
|
+
},
|
|
3240
|
+
async ({ projectId }) => {
|
|
3241
|
+
try {
|
|
3242
|
+
const result = await api.get("/cron/jobs", { params: { projectId } });
|
|
3243
|
+
return {
|
|
3244
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3245
|
+
};
|
|
3246
|
+
} catch (error) {
|
|
3247
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3248
|
+
return {
|
|
3249
|
+
content: [{ type: "text", text: `Failed to list all cron jobs: ${message}` }],
|
|
3250
|
+
isError: true
|
|
3251
|
+
};
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
);
|
|
3255
|
+
server.registerTool(
|
|
3256
|
+
"cron_jobs_status",
|
|
3257
|
+
{
|
|
3258
|
+
description: "Get cron queue status",
|
|
3259
|
+
inputSchema: {}
|
|
3260
|
+
},
|
|
3261
|
+
async () => {
|
|
3262
|
+
try {
|
|
3263
|
+
const result = await api.get("/cron/status");
|
|
3264
|
+
return {
|
|
3265
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3266
|
+
};
|
|
3267
|
+
} catch (error) {
|
|
3268
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3269
|
+
return {
|
|
3270
|
+
content: [{ type: "text", text: `Failed to get cron status: ${message}` }],
|
|
3271
|
+
isError: true
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
);
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
// libs/mcp-server/src/tools/webhooks.ts
|
|
3279
|
+
import { z as z10 } from "zod";
|
|
3280
|
+
var WEBHOOK_EVENT_TYPES = [
|
|
3281
|
+
"user.created",
|
|
3282
|
+
"user.updated",
|
|
3283
|
+
"user.deleted",
|
|
3284
|
+
"user.login",
|
|
3285
|
+
"user.email_verified",
|
|
3286
|
+
"user.password_reset_requested",
|
|
3287
|
+
"user.password_reset",
|
|
3288
|
+
"user.2fa_verified",
|
|
3289
|
+
"database.insert",
|
|
3290
|
+
"database.update",
|
|
3291
|
+
"database.delete",
|
|
3292
|
+
"storage.upload",
|
|
3293
|
+
"storage.delete",
|
|
3294
|
+
"project.created",
|
|
3295
|
+
"project.updated"
|
|
3296
|
+
];
|
|
3297
|
+
function registerWebhookTools(server, api) {
|
|
3298
|
+
server.registerTool(
|
|
3299
|
+
"webhooks_list",
|
|
3300
|
+
{
|
|
3301
|
+
description: "List all webhooks for a project",
|
|
3302
|
+
inputSchema: {
|
|
3303
|
+
projectId: z10.string()
|
|
3304
|
+
}
|
|
3305
|
+
},
|
|
3306
|
+
async ({ projectId }) => {
|
|
3307
|
+
try {
|
|
3308
|
+
const result = await api.get(`/projects/${encodeURIComponent(projectId)}/webhooks`);
|
|
3309
|
+
return {
|
|
3310
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3311
|
+
};
|
|
3312
|
+
} catch (error) {
|
|
3313
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3314
|
+
return {
|
|
3315
|
+
content: [{ type: "text", text: `Failed to list webhooks: ${message}` }],
|
|
3316
|
+
isError: true
|
|
3317
|
+
};
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
);
|
|
3321
|
+
server.registerTool(
|
|
3322
|
+
"webhooks_get",
|
|
3323
|
+
{
|
|
3324
|
+
description: "Get a single webhook by ID",
|
|
3325
|
+
inputSchema: {
|
|
3326
|
+
projectId: z10.string(),
|
|
3327
|
+
webhookId: z10.string()
|
|
3328
|
+
}
|
|
3329
|
+
},
|
|
3330
|
+
async ({ projectId, webhookId }) => {
|
|
3331
|
+
try {
|
|
3332
|
+
const result = await api.get(`/projects/${encodeURIComponent(projectId)}/webhooks/${encodeURIComponent(webhookId)}`);
|
|
3333
|
+
return {
|
|
3334
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3335
|
+
};
|
|
3336
|
+
} catch (error) {
|
|
3337
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3338
|
+
return {
|
|
3339
|
+
content: [{ type: "text", text: `Failed to get webhook: ${message}` }],
|
|
3340
|
+
isError: true
|
|
3341
|
+
};
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
);
|
|
3345
|
+
server.registerTool(
|
|
3346
|
+
"webhooks_create",
|
|
3347
|
+
{
|
|
3348
|
+
description: "Create a new webhook for a project. WARNING: The response includes a secret that will be visible in the conversation context. Events include: user.created, user.updated, user.deleted, user.login, user.email_verified, user.password_reset_requested, user.password_reset, user.2fa_verified, database.insert, database.update, database.delete, storage.upload, storage.delete, project.created, project.updated.",
|
|
3349
|
+
inputSchema: {
|
|
3350
|
+
projectId: z10.string(),
|
|
3351
|
+
url: z10.string().url(),
|
|
3352
|
+
events: z10.array(z10.enum(WEBHOOK_EVENT_TYPES)).min(1),
|
|
3353
|
+
description: z10.string().optional(),
|
|
3354
|
+
headers: z10.record(z10.string(), z10.string()).optional(),
|
|
3355
|
+
active: z10.boolean().optional()
|
|
3356
|
+
}
|
|
3357
|
+
},
|
|
3358
|
+
async ({ projectId, url, events, description, headers, active }) => {
|
|
3359
|
+
try {
|
|
3360
|
+
const body = { url, events };
|
|
3361
|
+
if (description !== void 0) body.description = description;
|
|
3362
|
+
if (headers !== void 0) body.headers = headers;
|
|
3363
|
+
if (active !== void 0) body.active = active;
|
|
3364
|
+
const result = await api.post(`/projects/${encodeURIComponent(projectId)}/webhooks`, { body });
|
|
3365
|
+
return {
|
|
3366
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3367
|
+
};
|
|
3368
|
+
} catch (error) {
|
|
3369
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3370
|
+
return {
|
|
3371
|
+
content: [{ type: "text", text: `Failed to create webhook: ${message}` }],
|
|
3372
|
+
isError: true
|
|
3373
|
+
};
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
);
|
|
3377
|
+
server.registerTool(
|
|
3378
|
+
"webhooks_update",
|
|
3379
|
+
{
|
|
3380
|
+
description: "Update an existing webhook",
|
|
3381
|
+
inputSchema: {
|
|
3382
|
+
projectId: z10.string(),
|
|
3383
|
+
webhookId: z10.string(),
|
|
3384
|
+
url: z10.string().url().optional(),
|
|
3385
|
+
events: z10.array(z10.enum(WEBHOOK_EVENT_TYPES)).min(1).optional(),
|
|
3386
|
+
description: z10.string().optional(),
|
|
3387
|
+
headers: z10.record(z10.string(), z10.string()).optional(),
|
|
3388
|
+
active: z10.boolean().optional()
|
|
3389
|
+
}
|
|
3390
|
+
},
|
|
3391
|
+
async ({ projectId, webhookId, url, events, description, headers, active }) => {
|
|
3392
|
+
try {
|
|
3393
|
+
const body = {};
|
|
3394
|
+
if (url !== void 0) body.url = url;
|
|
3395
|
+
if (events !== void 0) body.events = events;
|
|
3396
|
+
if (description !== void 0) body.description = description;
|
|
3397
|
+
if (headers !== void 0) body.headers = headers;
|
|
3398
|
+
if (active !== void 0) body.active = active;
|
|
3399
|
+
if (Object.keys(body).length === 0) {
|
|
3400
|
+
return {
|
|
3401
|
+
content: [{ type: "text", text: "At least one field must be provided" }],
|
|
3402
|
+
isError: true
|
|
3403
|
+
};
|
|
3404
|
+
}
|
|
3405
|
+
const result = await api.patch(`/projects/${encodeURIComponent(projectId)}/webhooks/${encodeURIComponent(webhookId)}`, { body });
|
|
3406
|
+
return {
|
|
3407
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3408
|
+
};
|
|
3409
|
+
} catch (error) {
|
|
3410
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3411
|
+
return {
|
|
3412
|
+
content: [{ type: "text", text: `Failed to update webhook: ${message}` }],
|
|
3413
|
+
isError: true
|
|
3414
|
+
};
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
);
|
|
3418
|
+
server.registerTool(
|
|
3419
|
+
"webhooks_delete",
|
|
3420
|
+
{
|
|
3421
|
+
description: "Delete a webhook",
|
|
3422
|
+
inputSchema: {
|
|
3423
|
+
projectId: z10.string(),
|
|
3424
|
+
webhookId: z10.string()
|
|
3425
|
+
}
|
|
3426
|
+
},
|
|
3427
|
+
async ({ projectId, webhookId }) => {
|
|
3428
|
+
try {
|
|
3429
|
+
const result = await api.delete(`/projects/${encodeURIComponent(projectId)}/webhooks/${encodeURIComponent(webhookId)}`);
|
|
3430
|
+
return {
|
|
3431
|
+
content: [{ type: "text", text: result ? JSON.stringify(result, null, 2) : "Webhook deleted" }]
|
|
3432
|
+
};
|
|
3433
|
+
} catch (error) {
|
|
3434
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3435
|
+
return {
|
|
3436
|
+
content: [{ type: "text", text: `Failed to delete webhook: ${message}` }],
|
|
3437
|
+
isError: true
|
|
3438
|
+
};
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
);
|
|
3442
|
+
server.registerTool(
|
|
3443
|
+
"webhooks_test",
|
|
3444
|
+
{
|
|
3445
|
+
description: "Send a test ping to a webhook",
|
|
3446
|
+
inputSchema: {
|
|
3447
|
+
projectId: z10.string(),
|
|
3448
|
+
webhookId: z10.string()
|
|
3449
|
+
}
|
|
3450
|
+
},
|
|
3451
|
+
async ({ projectId, webhookId }) => {
|
|
3452
|
+
try {
|
|
3453
|
+
const result = await api.post(`/projects/${encodeURIComponent(projectId)}/webhooks/${encodeURIComponent(webhookId)}/test`);
|
|
3454
|
+
return {
|
|
3455
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3456
|
+
};
|
|
3457
|
+
} catch (error) {
|
|
3458
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3459
|
+
return {
|
|
3460
|
+
content: [{ type: "text", text: `Failed to test webhook: ${message}` }],
|
|
3461
|
+
isError: true
|
|
3462
|
+
};
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
);
|
|
3466
|
+
server.registerTool(
|
|
3467
|
+
"webhooks_deliveries",
|
|
3468
|
+
{
|
|
3469
|
+
description: "List webhook delivery logs for a project",
|
|
3470
|
+
inputSchema: {
|
|
3471
|
+
projectId: z10.string(),
|
|
3472
|
+
webhookId: z10.string().optional(),
|
|
3473
|
+
limit: z10.number().int().min(1).max(100).optional(),
|
|
3474
|
+
offset: z10.number().int().min(0).optional()
|
|
3475
|
+
}
|
|
3476
|
+
},
|
|
3477
|
+
async ({ projectId, webhookId, limit, offset }) => {
|
|
3478
|
+
try {
|
|
3479
|
+
const result = await api.get(`/projects/${encodeURIComponent(projectId)}/webhook-deliveries`, { params: { webhookId, limit, offset } });
|
|
3480
|
+
return {
|
|
3481
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3482
|
+
};
|
|
3483
|
+
} catch (error) {
|
|
3484
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3485
|
+
return {
|
|
3486
|
+
content: [{ type: "text", text: `Failed to list webhook deliveries: ${message}` }],
|
|
3487
|
+
isError: true
|
|
3488
|
+
};
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
);
|
|
3492
|
+
server.registerTool(
|
|
3493
|
+
"webhooks_delivery_retry",
|
|
3494
|
+
{
|
|
3495
|
+
description: "Retry a failed webhook delivery",
|
|
3496
|
+
inputSchema: {
|
|
3497
|
+
projectId: z10.string(),
|
|
3498
|
+
deliveryId: z10.string()
|
|
3499
|
+
}
|
|
3500
|
+
},
|
|
3501
|
+
async ({ projectId, deliveryId }) => {
|
|
3502
|
+
try {
|
|
3503
|
+
const result = await api.post(`/projects/${encodeURIComponent(projectId)}/webhook-deliveries/${encodeURIComponent(deliveryId)}/retry`);
|
|
3504
|
+
return {
|
|
3505
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3506
|
+
};
|
|
3507
|
+
} catch (error) {
|
|
3508
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3509
|
+
return {
|
|
3510
|
+
content: [{ type: "text", text: `Failed to retry webhook delivery: ${message}` }],
|
|
3511
|
+
isError: true
|
|
3512
|
+
};
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
);
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
// libs/mcp-server/src/tools/notifications.ts
|
|
3519
|
+
import { z as z11 } from "zod";
|
|
3520
|
+
function registerNotificationTools(server, api) {
|
|
3521
|
+
server.registerTool(
|
|
3522
|
+
"notification_templates_list",
|
|
3523
|
+
{
|
|
3524
|
+
description: "List all notification templates for a project (includes system defaults)",
|
|
3525
|
+
inputSchema: {
|
|
3526
|
+
projectId: z11.string()
|
|
3527
|
+
}
|
|
3528
|
+
},
|
|
3529
|
+
async ({ projectId }) => {
|
|
3530
|
+
try {
|
|
3531
|
+
const result = await api.get(`/projects/${encodeURIComponent(projectId)}/notification-templates`);
|
|
3532
|
+
return {
|
|
3533
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3534
|
+
};
|
|
3535
|
+
} catch (error) {
|
|
3536
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3537
|
+
return {
|
|
3538
|
+
content: [{ type: "text", text: `Failed to list notification templates: ${message}` }],
|
|
3539
|
+
isError: true
|
|
3540
|
+
};
|
|
3541
|
+
}
|
|
3542
|
+
}
|
|
3543
|
+
);
|
|
3544
|
+
server.registerTool(
|
|
3545
|
+
"notification_templates_get",
|
|
3546
|
+
{
|
|
3547
|
+
description: "Get a single notification template by ID",
|
|
3548
|
+
inputSchema: {
|
|
3549
|
+
id: z11.string()
|
|
3550
|
+
}
|
|
3551
|
+
},
|
|
3552
|
+
async ({ id }) => {
|
|
3553
|
+
try {
|
|
3554
|
+
const result = await api.get(`/notification-templates/${encodeURIComponent(id)}`);
|
|
3555
|
+
return {
|
|
3556
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3557
|
+
};
|
|
3558
|
+
} catch (error) {
|
|
3559
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3560
|
+
return {
|
|
3561
|
+
content: [{ type: "text", text: `Failed to get notification template: ${message}` }],
|
|
3562
|
+
isError: true
|
|
3563
|
+
};
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
);
|
|
3567
|
+
server.registerTool(
|
|
3568
|
+
"notification_templates_create",
|
|
3569
|
+
{
|
|
3570
|
+
description: "Create a new project-specific notification template. Variables use Handlebars syntax in titleTemplate and bodyTemplate.",
|
|
3571
|
+
inputSchema: {
|
|
3572
|
+
projectId: z11.string(),
|
|
3573
|
+
type: z11.string(),
|
|
3574
|
+
name: z11.string(),
|
|
3575
|
+
titleTemplate: z11.string(),
|
|
3576
|
+
bodyTemplate: z11.string(),
|
|
3577
|
+
icon: z11.string().optional(),
|
|
3578
|
+
defaultUrl: z11.string().optional(),
|
|
3579
|
+
variables: z11.array(z11.string()).optional(),
|
|
3580
|
+
isActive: z11.boolean().optional()
|
|
3581
|
+
}
|
|
3582
|
+
},
|
|
3583
|
+
async ({ projectId, type, name, titleTemplate, bodyTemplate, icon, defaultUrl, variables, isActive }) => {
|
|
3584
|
+
try {
|
|
3585
|
+
const body = { projectId, type, name, titleTemplate, bodyTemplate };
|
|
3586
|
+
if (icon !== void 0) body.icon = icon;
|
|
3587
|
+
if (defaultUrl !== void 0) body.defaultUrl = defaultUrl;
|
|
3588
|
+
if (variables !== void 0) body.variables = variables;
|
|
3589
|
+
if (isActive !== void 0) body.isActive = isActive;
|
|
3590
|
+
const result = await api.post(`/projects/${encodeURIComponent(projectId)}/notification-templates`, { body });
|
|
3591
|
+
return {
|
|
3592
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3593
|
+
};
|
|
3594
|
+
} catch (error) {
|
|
3595
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3596
|
+
return {
|
|
3597
|
+
content: [{ type: "text", text: `Failed to create notification template: ${message}` }],
|
|
3598
|
+
isError: true
|
|
3599
|
+
};
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
);
|
|
3603
|
+
server.registerTool(
|
|
3604
|
+
"notification_templates_update",
|
|
3605
|
+
{
|
|
3606
|
+
description: "Update an existing notification template",
|
|
3607
|
+
inputSchema: {
|
|
3608
|
+
id: z11.string(),
|
|
3609
|
+
name: z11.string().optional(),
|
|
3610
|
+
titleTemplate: z11.string().optional(),
|
|
3611
|
+
bodyTemplate: z11.string().optional(),
|
|
3612
|
+
icon: z11.string().optional(),
|
|
3613
|
+
defaultUrl: z11.string().optional(),
|
|
3614
|
+
variables: z11.array(z11.string()).optional(),
|
|
3615
|
+
isActive: z11.boolean().optional()
|
|
3616
|
+
}
|
|
3617
|
+
},
|
|
3618
|
+
async ({ id, name, titleTemplate, bodyTemplate, icon, defaultUrl, variables, isActive }) => {
|
|
3619
|
+
try {
|
|
3620
|
+
const body = {};
|
|
3621
|
+
if (name !== void 0) body.name = name;
|
|
3622
|
+
if (titleTemplate !== void 0) body.titleTemplate = titleTemplate;
|
|
3623
|
+
if (bodyTemplate !== void 0) body.bodyTemplate = bodyTemplate;
|
|
3624
|
+
if (icon !== void 0) body.icon = icon;
|
|
3625
|
+
if (defaultUrl !== void 0) body.defaultUrl = defaultUrl;
|
|
3626
|
+
if (variables !== void 0) body.variables = variables;
|
|
3627
|
+
if (isActive !== void 0) body.isActive = isActive;
|
|
3628
|
+
if (Object.keys(body).length === 0) {
|
|
3629
|
+
return {
|
|
3630
|
+
content: [{ type: "text", text: "At least one field must be provided" }],
|
|
3631
|
+
isError: true
|
|
3632
|
+
};
|
|
3633
|
+
}
|
|
3634
|
+
const result = await api.patch(`/notification-templates/${encodeURIComponent(id)}`, { body });
|
|
3635
|
+
return {
|
|
3636
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3637
|
+
};
|
|
3638
|
+
} catch (error) {
|
|
3639
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3640
|
+
return {
|
|
3641
|
+
content: [{ type: "text", text: `Failed to update notification template: ${message}` }],
|
|
3642
|
+
isError: true
|
|
3643
|
+
};
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
);
|
|
3647
|
+
server.registerTool(
|
|
3648
|
+
"notification_templates_delete",
|
|
3649
|
+
{
|
|
3650
|
+
description: "Delete a project notification template override (reverts to system default)",
|
|
3651
|
+
inputSchema: {
|
|
3652
|
+
id: z11.string()
|
|
3653
|
+
}
|
|
3654
|
+
},
|
|
3655
|
+
async ({ id }) => {
|
|
3656
|
+
try {
|
|
3657
|
+
const result = await api.delete(`/notification-templates/${encodeURIComponent(id)}`);
|
|
3658
|
+
return {
|
|
3659
|
+
content: [{ type: "text", text: result ? JSON.stringify(result, null, 2) : "Notification template deleted" }]
|
|
3660
|
+
};
|
|
3661
|
+
} catch (error) {
|
|
3662
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3663
|
+
return {
|
|
3664
|
+
content: [{ type: "text", text: `Failed to delete notification template: ${message}` }],
|
|
3665
|
+
isError: true
|
|
3666
|
+
};
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
);
|
|
3670
|
+
server.registerTool(
|
|
3671
|
+
"notification_templates_preview",
|
|
3672
|
+
{
|
|
3673
|
+
description: "Preview a compiled notification template with Handlebars variables applied",
|
|
3674
|
+
inputSchema: {
|
|
3675
|
+
titleTemplate: z11.string(),
|
|
3676
|
+
bodyTemplate: z11.string(),
|
|
3677
|
+
variables: z11.record(z11.string(), z11.string())
|
|
3678
|
+
}
|
|
3679
|
+
},
|
|
3680
|
+
async ({ titleTemplate, bodyTemplate, variables }) => {
|
|
3681
|
+
try {
|
|
3682
|
+
const result = await api.post("/notification-templates/preview", { body: { titleTemplate, bodyTemplate, variables } });
|
|
3683
|
+
return {
|
|
3684
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3685
|
+
};
|
|
3686
|
+
} catch (error) {
|
|
3687
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3688
|
+
return {
|
|
3689
|
+
content: [{ type: "text", text: `Failed to preview notification template: ${message}` }],
|
|
3690
|
+
isError: true
|
|
3691
|
+
};
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
);
|
|
3695
|
+
server.registerTool(
|
|
3696
|
+
"notification_config_get",
|
|
3697
|
+
{
|
|
3698
|
+
description: "Get push notification provider configuration for a project (VAPID, FCM, APNS). WARNING: Response may include sensitive credentials that will be visible in the conversation context.",
|
|
3699
|
+
inputSchema: {
|
|
3700
|
+
projectId: z11.string()
|
|
3701
|
+
}
|
|
3702
|
+
},
|
|
3703
|
+
async ({ projectId }) => {
|
|
3704
|
+
try {
|
|
3705
|
+
const result = await api.get(`/projects/${encodeURIComponent(projectId)}/notification-config`);
|
|
3706
|
+
return {
|
|
3707
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3708
|
+
};
|
|
3709
|
+
} catch (error) {
|
|
3710
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3711
|
+
return {
|
|
3712
|
+
content: [{ type: "text", text: `Failed to get notification config: ${message}` }],
|
|
3713
|
+
isError: true
|
|
3714
|
+
};
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
);
|
|
3718
|
+
server.registerTool(
|
|
3719
|
+
"notification_config_update",
|
|
3720
|
+
{
|
|
3721
|
+
description: "Update push notification provider configuration. Supports VAPID, FCM, and APNS providers. WARNING: privateKey and serviceAccountJson values are sensitive credentials that will be visible in the conversation context.",
|
|
3722
|
+
inputSchema: {
|
|
3723
|
+
projectId: z11.string(),
|
|
3724
|
+
vapid: z11.object({
|
|
3725
|
+
publicKey: z11.string().optional(),
|
|
3726
|
+
privateKey: z11.string().optional(),
|
|
3727
|
+
subject: z11.string().optional(),
|
|
3728
|
+
enabled: z11.boolean().optional()
|
|
3729
|
+
}).optional(),
|
|
3730
|
+
fcm: z11.object({
|
|
3731
|
+
serviceAccountJson: z11.string().optional(),
|
|
3732
|
+
enabled: z11.boolean().optional()
|
|
3733
|
+
}).optional(),
|
|
3734
|
+
apns: z11.object({
|
|
3735
|
+
keyId: z11.string().optional(),
|
|
3736
|
+
teamId: z11.string().optional(),
|
|
3737
|
+
privateKey: z11.string().optional(),
|
|
3738
|
+
bundleId: z11.string().optional(),
|
|
3739
|
+
production: z11.boolean().optional(),
|
|
3740
|
+
enabled: z11.boolean().optional()
|
|
3741
|
+
}).optional()
|
|
3742
|
+
}
|
|
3743
|
+
},
|
|
3744
|
+
async ({ projectId, vapid, fcm, apns }) => {
|
|
3745
|
+
try {
|
|
3746
|
+
const body = {};
|
|
3747
|
+
if (vapid !== void 0) body.vapid = vapid;
|
|
3748
|
+
if (fcm !== void 0) body.fcm = fcm;
|
|
3749
|
+
if (apns !== void 0) body.apns = apns;
|
|
3750
|
+
if (Object.keys(body).length === 0) {
|
|
3751
|
+
return {
|
|
3752
|
+
content: [{ type: "text", text: "At least one provider config must be provided" }],
|
|
3753
|
+
isError: true
|
|
3754
|
+
};
|
|
3755
|
+
}
|
|
3756
|
+
const result = await api.patch(`/projects/${encodeURIComponent(projectId)}/notification-config`, { body });
|
|
3757
|
+
return {
|
|
3758
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3759
|
+
};
|
|
3760
|
+
} catch (error) {
|
|
3761
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3762
|
+
return {
|
|
3763
|
+
content: [{ type: "text", text: `Failed to update notification config: ${message}` }],
|
|
3764
|
+
isError: true
|
|
3765
|
+
};
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
);
|
|
3769
|
+
server.registerTool(
|
|
3770
|
+
"notification_config_delete",
|
|
3771
|
+
{
|
|
3772
|
+
description: "Delete push notification provider configuration. Specify a provider to delete only that one, or omit to delete all.",
|
|
3773
|
+
inputSchema: {
|
|
3774
|
+
projectId: z11.string(),
|
|
3775
|
+
provider: z11.enum(["vapid", "fcm", "apns"]).optional()
|
|
3776
|
+
}
|
|
3777
|
+
},
|
|
3778
|
+
async ({ projectId, provider }) => {
|
|
3779
|
+
try {
|
|
3780
|
+
const result = await api.delete(`/projects/${encodeURIComponent(projectId)}/notification-config`, { params: { provider } });
|
|
3781
|
+
return {
|
|
3782
|
+
content: [{ type: "text", text: result ? JSON.stringify(result, null, 2) : "Notification config deleted" }]
|
|
3783
|
+
};
|
|
3784
|
+
} catch (error) {
|
|
3785
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3786
|
+
return {
|
|
3787
|
+
content: [{ type: "text", text: `Failed to delete notification config: ${message}` }],
|
|
3788
|
+
isError: true
|
|
3789
|
+
};
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
);
|
|
3793
|
+
server.registerTool(
|
|
3794
|
+
"notification_config_generate_vapid",
|
|
3795
|
+
{
|
|
3796
|
+
description: "Generate a new VAPID key pair for web push notifications. WARNING: The private key will be visible in the conversation context.",
|
|
3797
|
+
inputSchema: {}
|
|
3798
|
+
},
|
|
3799
|
+
async () => {
|
|
3800
|
+
try {
|
|
3801
|
+
const result = await api.post("/notification-config/generate-vapid-keys");
|
|
3802
|
+
return {
|
|
3803
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3804
|
+
};
|
|
3805
|
+
} catch (error) {
|
|
3806
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3807
|
+
return {
|
|
3808
|
+
content: [{ type: "text", text: `Failed to generate VAPID keys: ${message}` }],
|
|
3809
|
+
isError: true
|
|
3810
|
+
};
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
);
|
|
3814
|
+
server.registerTool(
|
|
3815
|
+
"notifications_send",
|
|
3816
|
+
{
|
|
3817
|
+
description: "Send a push notification to selected users",
|
|
3818
|
+
inputSchema: {
|
|
3819
|
+
projectId: z11.string(),
|
|
3820
|
+
userIds: z11.array(z11.string()).min(1),
|
|
3821
|
+
title: z11.string(),
|
|
3822
|
+
body: z11.string(),
|
|
3823
|
+
icon: z11.string().optional(),
|
|
3824
|
+
url: z11.string().optional()
|
|
3825
|
+
}
|
|
3826
|
+
},
|
|
3827
|
+
async ({ projectId, userIds, title, body: notifBody, icon, url }) => {
|
|
3828
|
+
try {
|
|
3829
|
+
const reqBody = { userIds, title, body: notifBody };
|
|
3830
|
+
if (icon !== void 0) reqBody.icon = icon;
|
|
3831
|
+
if (url !== void 0) reqBody.url = url;
|
|
3832
|
+
const result = await api.post(`/projects/${encodeURIComponent(projectId)}/notifications/send`, { body: reqBody });
|
|
3833
|
+
return {
|
|
3834
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3835
|
+
};
|
|
3836
|
+
} catch (error) {
|
|
3837
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3838
|
+
return {
|
|
3839
|
+
content: [{ type: "text", text: `Failed to send notification: ${message}` }],
|
|
3840
|
+
isError: true
|
|
3841
|
+
};
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
);
|
|
3845
|
+
server.registerTool(
|
|
3846
|
+
"notifications_broadcast",
|
|
3847
|
+
{
|
|
3848
|
+
description: "Broadcast a push notification to all subscribed users in a project",
|
|
3849
|
+
inputSchema: {
|
|
3850
|
+
projectId: z11.string(),
|
|
3851
|
+
title: z11.string(),
|
|
3852
|
+
body: z11.string(),
|
|
3853
|
+
icon: z11.string().optional(),
|
|
3854
|
+
url: z11.string().optional()
|
|
3855
|
+
}
|
|
3856
|
+
},
|
|
3857
|
+
async ({ projectId, title, body: notifBody, icon, url }) => {
|
|
3858
|
+
try {
|
|
3859
|
+
const reqBody = { title, body: notifBody };
|
|
3860
|
+
if (icon !== void 0) reqBody.icon = icon;
|
|
3861
|
+
if (url !== void 0) reqBody.url = url;
|
|
3862
|
+
const result = await api.post(`/projects/${encodeURIComponent(projectId)}/notifications/broadcast`, { body: reqBody });
|
|
3863
|
+
return {
|
|
3864
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3865
|
+
};
|
|
3866
|
+
} catch (error) {
|
|
3867
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3868
|
+
return {
|
|
3869
|
+
content: [{ type: "text", text: `Failed to broadcast notification: ${message}` }],
|
|
3870
|
+
isError: true
|
|
3871
|
+
};
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
);
|
|
3875
|
+
}
|
|
3876
|
+
|
|
3877
|
+
// libs/mcp-server/src/tools/auditLogs.ts
|
|
3878
|
+
import { z as z12 } from "zod";
|
|
3879
|
+
function registerAuditLogTools(server, api) {
|
|
3880
|
+
server.registerTool(
|
|
3881
|
+
"audit_logs_list",
|
|
3882
|
+
{
|
|
3883
|
+
description: "List audit logs for a project. Filter by entity type, action, and date range.",
|
|
3884
|
+
inputSchema: {
|
|
3885
|
+
projectId: z12.string(),
|
|
3886
|
+
entityType: z12.enum(["user", "client", "project", "auth", "database", "storage"]).optional(),
|
|
3887
|
+
action: z12.string().optional(),
|
|
3888
|
+
startDate: z12.string().datetime().optional(),
|
|
3889
|
+
endDate: z12.string().datetime().optional(),
|
|
3890
|
+
limit: z12.number().int().min(1).max(100).optional(),
|
|
3891
|
+
offset: z12.number().int().min(0).optional()
|
|
3892
|
+
}
|
|
3893
|
+
},
|
|
3894
|
+
async ({ projectId, entityType, action, startDate, endDate, limit, offset }) => {
|
|
3895
|
+
try {
|
|
3896
|
+
const result = await api.get(`/projects/${encodeURIComponent(projectId)}/audit-logs`, {
|
|
3897
|
+
params: { entityType, action, startDate, endDate, limit, offset }
|
|
3898
|
+
});
|
|
3899
|
+
return {
|
|
3900
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3901
|
+
};
|
|
3902
|
+
} catch (error) {
|
|
3903
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3904
|
+
return {
|
|
3905
|
+
content: [{ type: "text", text: `Failed to list audit logs: ${message}` }],
|
|
3906
|
+
isError: true
|
|
3907
|
+
};
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
);
|
|
3911
|
+
server.registerTool(
|
|
3912
|
+
"audit_logs_get",
|
|
3913
|
+
{
|
|
3914
|
+
description: "Get a single audit log entry by ID",
|
|
3915
|
+
inputSchema: {
|
|
3916
|
+
projectId: z12.string(),
|
|
3917
|
+
logId: z12.string()
|
|
3918
|
+
}
|
|
3919
|
+
},
|
|
3920
|
+
async ({ projectId, logId }) => {
|
|
3921
|
+
try {
|
|
3922
|
+
const result = await api.get(`/projects/${encodeURIComponent(projectId)}/audit-logs/${encodeURIComponent(logId)}`);
|
|
3923
|
+
return {
|
|
3924
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
3925
|
+
};
|
|
3926
|
+
} catch (error) {
|
|
3927
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3928
|
+
return {
|
|
3929
|
+
content: [{ type: "text", text: `Failed to get audit log: ${message}` }],
|
|
3930
|
+
isError: true
|
|
3931
|
+
};
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
);
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
// libs/mcp-server/src/tools/index.ts
|
|
3938
|
+
function registerAllTools(server, api) {
|
|
3939
|
+
registerAuthTools(server, api);
|
|
3940
|
+
registerProjectTools(server, api);
|
|
3941
|
+
registerClientTools(server, api);
|
|
3942
|
+
registerDatabaseTools(server, api);
|
|
3943
|
+
registerHostingTools(server, api);
|
|
3944
|
+
registerStorageTools(server, api);
|
|
3945
|
+
registerFunctionTools(server, api);
|
|
3946
|
+
registerEmailTemplateTools(server, api);
|
|
3947
|
+
registerCronJobTools(server, api);
|
|
3948
|
+
registerWebhookTools(server, api);
|
|
3949
|
+
registerNotificationTools(server, api);
|
|
3950
|
+
registerAuditLogTools(server, api);
|
|
3951
|
+
}
|
|
3952
|
+
|
|
3953
|
+
// libs/mcp-server/src/prompts/database.ts
|
|
3954
|
+
import { z as z13 } from "zod";
|
|
3955
|
+
function registerDatabasePrompts(server) {
|
|
3956
|
+
server.registerPrompt(
|
|
3957
|
+
"setup-collection",
|
|
3958
|
+
{
|
|
3959
|
+
title: "Setup Database Collection",
|
|
3960
|
+
description: "Guide for creating a new database collection with proper security rules. Collections MUST have rules defined before data can be inserted.",
|
|
3961
|
+
argsSchema: {
|
|
3962
|
+
projectId: z13.string().describe("Project ID"),
|
|
3963
|
+
collectionName: z13.string().describe("Name of the collection to create"),
|
|
3964
|
+
access: z13.enum(["public-read", "authenticated", "admin-only"]).optional().describe("Access level (default: admin-only)")
|
|
3965
|
+
}
|
|
3966
|
+
},
|
|
3967
|
+
async ({ projectId, collectionName, access }) => {
|
|
3968
|
+
const accessLevel = access || "admin-only";
|
|
3969
|
+
const rules = {
|
|
3970
|
+
"public-read": {
|
|
3971
|
+
".create": "true",
|
|
3972
|
+
".read": "true",
|
|
3973
|
+
".update": "false",
|
|
3974
|
+
".delete": "auth.role === 'owner' || auth.role === 'admin'"
|
|
3975
|
+
},
|
|
3976
|
+
authenticated: {
|
|
3977
|
+
".create": "!!auth.uid",
|
|
3978
|
+
".read": "!!auth.uid",
|
|
3979
|
+
".update": "!!auth.uid",
|
|
3980
|
+
".delete": "auth.role === 'owner' || auth.role === 'admin'"
|
|
3981
|
+
},
|
|
3982
|
+
"admin-only": {
|
|
3983
|
+
".create": "auth.role === 'owner' || auth.role === 'admin'",
|
|
3984
|
+
".read": "auth.role === 'owner' || auth.role === 'admin'",
|
|
3985
|
+
".update": "auth.role === 'owner' || auth.role === 'admin'",
|
|
3986
|
+
".delete": "auth.role === 'owner' || auth.role === 'admin'"
|
|
3987
|
+
}
|
|
3988
|
+
};
|
|
3989
|
+
const collectionRules = rules[accessLevel];
|
|
3990
|
+
const rulesJson = JSON.stringify(collectionRules, null, 2);
|
|
3991
|
+
return {
|
|
3992
|
+
messages: [
|
|
3993
|
+
{
|
|
3994
|
+
role: "user",
|
|
3995
|
+
content: {
|
|
3996
|
+
type: "text",
|
|
3997
|
+
text: [
|
|
3998
|
+
`Set up a new database collection "${collectionName}" in project ${projectId}.`,
|
|
3999
|
+
"",
|
|
4000
|
+
"IMPORTANT WORKFLOW:",
|
|
4001
|
+
"1. First, get the current rules with database_rules_get",
|
|
4002
|
+
"2. Add the new collection rules to the existing rules object",
|
|
4003
|
+
"3. Set the updated rules with database_rules_set \u2014 this automatically creates the collection",
|
|
4004
|
+
'4. Do NOT try to insert documents before the rules are set \u2014 it will fail with "Rule denied"',
|
|
4005
|
+
"",
|
|
4006
|
+
`Suggested rules for "${collectionName}" (${accessLevel}):`,
|
|
4007
|
+
rulesJson,
|
|
4008
|
+
"",
|
|
4009
|
+
"The $other catch-all rule should deny access to undefined collections:",
|
|
4010
|
+
' "$other": { ".read": "false", ".write": "false" }',
|
|
4011
|
+
"",
|
|
4012
|
+
"After rules are set, the collection is ready for use via:",
|
|
4013
|
+
"- database_documents_insert",
|
|
4014
|
+
"- database_documents_find",
|
|
4015
|
+
'- Serverless functions using spacelr.db.collection("' + collectionName + '")'
|
|
4016
|
+
].join("\n")
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
]
|
|
4020
|
+
};
|
|
4021
|
+
}
|
|
4022
|
+
);
|
|
4023
|
+
server.registerPrompt(
|
|
4024
|
+
"database-workflow",
|
|
4025
|
+
{
|
|
4026
|
+
title: "Database Workflow Guide",
|
|
4027
|
+
description: "Explains how the Spacelr database system works: rules-first approach, collection lifecycle, and security model.",
|
|
4028
|
+
argsSchema: {
|
|
4029
|
+
projectId: z13.string().describe("Project ID")
|
|
4030
|
+
}
|
|
4031
|
+
},
|
|
4032
|
+
async ({ projectId }) => {
|
|
4033
|
+
return {
|
|
4034
|
+
messages: [
|
|
4035
|
+
{
|
|
4036
|
+
role: "user",
|
|
4037
|
+
content: {
|
|
4038
|
+
type: "text",
|
|
2656
4039
|
text: [
|
|
2657
4040
|
`Guide me through the Spacelr database system for project ${projectId}.`,
|
|
2658
4041
|
"",
|
|
@@ -2692,7 +4075,7 @@ function registerDatabasePrompts(server) {
|
|
|
2692
4075
|
}
|
|
2693
4076
|
|
|
2694
4077
|
// libs/mcp-server/src/prompts/functions.ts
|
|
2695
|
-
import { z as
|
|
4078
|
+
import { z as z14 } from "zod";
|
|
2696
4079
|
function registerFunctionPrompts(server) {
|
|
2697
4080
|
server.registerPrompt(
|
|
2698
4081
|
"deploy-function",
|
|
@@ -2700,9 +4083,9 @@ function registerFunctionPrompts(server) {
|
|
|
2700
4083
|
title: "Deploy a Serverless Function",
|
|
2701
4084
|
description: "Step-by-step guide for creating, deploying, and testing a serverless function. Covers the full lifecycle from creation to execution.",
|
|
2702
4085
|
argsSchema: {
|
|
2703
|
-
projectId:
|
|
2704
|
-
name:
|
|
2705
|
-
useCase:
|
|
4086
|
+
projectId: z14.string().describe("Project ID"),
|
|
4087
|
+
name: z14.string().describe("Function name"),
|
|
4088
|
+
useCase: z14.string().optional().describe('What the function should do (e.g. "fetch data from API and store in DB")')
|
|
2706
4089
|
}
|
|
2707
4090
|
},
|
|
2708
4091
|
async ({ projectId, name, useCase }) => {
|
|
@@ -2724,7 +4107,10 @@ function registerFunctionPrompts(server) {
|
|
|
2724
4107
|
" Optional: cronExpression for scheduled execution",
|
|
2725
4108
|
"",
|
|
2726
4109
|
"2. WRITE the code:",
|
|
2727
|
-
" Create an index.js file
|
|
4110
|
+
" Create an index.js file with plain JavaScript (ES2022).",
|
|
4111
|
+
" The runtime is an isolated V8 sandbox \u2014 NOT Node.js.",
|
|
4112
|
+
" No require/import, no Node.js built-ins (fs, path, crypto, Buffer, process).",
|
|
4113
|
+
" Top-level await is supported. Available APIs inside the sandbox:",
|
|
2728
4114
|
"",
|
|
2729
4115
|
" - console.log/warn/error/info() \u2014 captured to execution logs",
|
|
2730
4116
|
" - await fetch(url, options) \u2014 HTTP client (10s timeout, 5MB limit)",
|
|
@@ -2737,6 +4123,10 @@ function registerFunctionPrompts(server) {
|
|
|
2737
4123
|
" - await spacelr.email.send/sendRaw() \u2014 send template or raw emails",
|
|
2738
4124
|
" - await spacelr.notifications.send/sendMany() \u2014 push notifications",
|
|
2739
4125
|
"",
|
|
4126
|
+
" Trigger context globals (depending on trigger type):",
|
|
4127
|
+
" - event: { type, data, timestamp } \u2014 for event-triggered executions",
|
|
4128
|
+
" - payload: { ... } \u2014 for webhook-triggered executions (the POST body)",
|
|
4129
|
+
"",
|
|
2740
4130
|
" IMPORTANT: Top-level await is supported. No imports/require available.",
|
|
2741
4131
|
"",
|
|
2742
4132
|
"3. IF USING spacelr.db:",
|
|
@@ -2745,9 +4135,11 @@ function registerFunctionPrompts(server) {
|
|
|
2745
4135
|
' Without rules, inserts will fail with "Rule denied".',
|
|
2746
4136
|
"",
|
|
2747
4137
|
"4. DEPLOY the code:",
|
|
2748
|
-
"
|
|
2749
|
-
|
|
2750
|
-
"
|
|
4138
|
+
" Use the functions_deploy tool to upload code directly:",
|
|
4139
|
+
' functions_deploy({ projectId, functionId, files: { "index.js": "..." } })',
|
|
4140
|
+
" This bundles the files into a ZIP and uploads them automatically.",
|
|
4141
|
+
' You can deploy multiple files: { "index.js": "...", "lib.js": "..." }',
|
|
4142
|
+
" Optionally set entryPoint if your main file is not index.js.",
|
|
2751
4143
|
"",
|
|
2752
4144
|
"5. TRIGGER:",
|
|
2753
4145
|
" Functions support multiple trigger types:",
|
|
@@ -2790,6 +4182,31 @@ function registerFunctionPrompts(server) {
|
|
|
2790
4182
|
text: [
|
|
2791
4183
|
"Show me the complete API reference for the Spacelr function sandbox.",
|
|
2792
4184
|
"",
|
|
4185
|
+
"\u2550\u2550\u2550 RUNTIME ENVIRONMENT \u2550\u2550\u2550",
|
|
4186
|
+
"Functions run in an isolated V8 sandbox (via isolated-vm), NOT in Node.js.",
|
|
4187
|
+
"This means:",
|
|
4188
|
+
" - No require() or import statements \u2014 all APIs are pre-injected globals",
|
|
4189
|
+
" - No Node.js built-ins (fs, path, crypto, Buffer, process, etc.)",
|
|
4190
|
+
" - No setTimeout, setInterval, or Promise.race",
|
|
4191
|
+
" - No Function constructor, eval(), WebAssembly, or Proxy",
|
|
4192
|
+
" - Top-level await IS supported (code runs in an async IIFE)",
|
|
4193
|
+
" - Standard JS built-ins work: JSON, Math, Date, Array, Map, Set, RegExp, etc.",
|
|
4194
|
+
" - Use the built-in fetch() for HTTP calls (not node-fetch or axios)",
|
|
4195
|
+
" - Use env.get() for secrets (not process.env)",
|
|
4196
|
+
" - Use kv.get/set() for persistence across executions (not file system)",
|
|
4197
|
+
"",
|
|
4198
|
+
"Write plain JavaScript (ES2022). The code structure is simple:",
|
|
4199
|
+
" // Top-level code runs immediately",
|
|
4200
|
+
' const data = await fetch("https://api.example.com/data");',
|
|
4201
|
+
" const json = JSON.parse(data.body);",
|
|
4202
|
+
' console.log("Fetched", json.length, "items");',
|
|
4203
|
+
"",
|
|
4204
|
+
"\u2550\u2550\u2550 TRIGGER CONTEXT \u2550\u2550\u2550",
|
|
4205
|
+
"Depending on how the function was triggered, these globals may be available:",
|
|
4206
|
+
" - event: { type, data, timestamp } \u2014 for event-triggered executions",
|
|
4207
|
+
" - payload: { ... } \u2014 for webhook-triggered executions (the POST body)",
|
|
4208
|
+
"For manual and cron triggers, neither is set.",
|
|
4209
|
+
"",
|
|
2793
4210
|
"\u2550\u2550\u2550 CONSOLE \u2550\u2550\u2550",
|
|
2794
4211
|
"console.log(...args) \u2014 log level",
|
|
2795
4212
|
"console.info(...args) \u2014 info level",
|
|
@@ -2822,11 +4239,26 @@ function registerFunctionPrompts(server) {
|
|
|
2822
4239
|
"",
|
|
2823
4240
|
"\u2550\u2550\u2550 DATABASE \u2550\u2550\u2550",
|
|
2824
4241
|
"const docs = await spacelr.db.collection(name).find(filter?, options?)",
|
|
2825
|
-
|
|
4242
|
+
' filter: MongoDB-style query (e.g. { status: "active", age: { $gt: 18 } })',
|
|
4243
|
+
" options: { sort: { createdAt: -1 }, limit: 10, offset: 0 }",
|
|
4244
|
+
" returns: array of documents",
|
|
4245
|
+
"",
|
|
2826
4246
|
"await spacelr.db.collection(name).insertOne(doc)",
|
|
4247
|
+
' doc: plain object (e.g. { name: "Max", email: "max@example.com" })',
|
|
4248
|
+
" returns: the inserted document with _id",
|
|
4249
|
+
"",
|
|
2827
4250
|
"await spacelr.db.collection(name).insertMany(docs)",
|
|
4251
|
+
" docs: array of objects",
|
|
4252
|
+
" returns: array of inserted documents",
|
|
4253
|
+
"",
|
|
4254
|
+
"Collection names: alphanumeric + underscore only, max 128 chars.",
|
|
2828
4255
|
"Scoped to the function's project. Requires database rules to be set first.",
|
|
2829
4256
|
"",
|
|
4257
|
+
"Example \u2014 read and write:",
|
|
4258
|
+
' const users = await spacelr.db.collection("users").find({ status: "active" }, { limit: 5 });',
|
|
4259
|
+
' console.log("Found", users.length, "active users");',
|
|
4260
|
+
' await spacelr.db.collection("logs").insertOne({ action: "check", count: users.length, at: new Date().toISOString() });',
|
|
4261
|
+
"",
|
|
2830
4262
|
"\u2550\u2550\u2550 STORAGE \u2550\u2550\u2550",
|
|
2831
4263
|
"await spacelr.storage.list(options?) \u2192 FileInfo[]",
|
|
2832
4264
|
"await spacelr.storage.getInfo(fileId) \u2192 FileInfo",
|