@spacelr/mcp 0.0.2 → 0.0.5

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 CHANGED
@@ -76,13 +76,13 @@ async function refreshToken(credentials) {
76
76
  clearTimeout(timer);
77
77
  }
78
78
  }
79
- async function resolveAuthToken() {
79
+ async function resolveAuthToken(forceRefresh = false) {
80
80
  const envToken = process.env["SPACELR_AUTH_TOKEN"];
81
81
  if (envToken) return envToken;
82
82
  const credentials = readStoredCredentials();
83
83
  if (!credentials) return null;
84
84
  const isExpired = Date.now() >= credentials.expiresAt - 6e4;
85
- if (!isExpired) return credentials.accessToken;
85
+ if (!forceRefresh && !isExpired) return credentials.accessToken;
86
86
  return refreshToken(credentials);
87
87
  }
88
88
  function warnIfInsecureUrl(url) {
@@ -119,6 +119,14 @@ function resolveApiUrl(credentials) {
119
119
  console.error(`Warning: No API URL configured, using default: ${defaultUrl}`);
120
120
  return defaultUrl;
121
121
  }
122
+ function resolveApiBaseUrl() {
123
+ try {
124
+ const credentials = readStoredCredentials();
125
+ return resolveApiUrl(credentials);
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
122
130
  async function loadConfig() {
123
131
  const credentials = readStoredCredentials();
124
132
  const authToken = await resolveAuthToken();
@@ -175,9 +183,9 @@ var ApiClient = class {
175
183
  async delete(path2, opts) {
176
184
  return this.requestWithRetry("DELETE", path2, opts);
177
185
  }
178
- async refreshAuthToken() {
186
+ async refreshAuthToken(force = false) {
179
187
  if (!this.refreshPromise) {
180
- this.refreshPromise = resolveAuthToken().finally(() => {
188
+ this.refreshPromise = resolveAuthToken(force).finally(() => {
181
189
  this.refreshPromise = null;
182
190
  });
183
191
  }
@@ -189,7 +197,14 @@ var ApiClient = class {
189
197
  } catch (error) {
190
198
  if (error instanceof ApiError && error.status === 401) {
191
199
  const currentToken = this.headers["Authorization"];
192
- const newToken = await this.refreshAuthToken();
200
+ let newToken = await this.refreshAuthToken();
201
+ const newBaseUrl = resolveApiBaseUrl();
202
+ if (newBaseUrl && newBaseUrl !== this.baseUrl) {
203
+ this.baseUrl = newBaseUrl;
204
+ }
205
+ if (newToken && `Bearer ${newToken}` === currentToken) {
206
+ newToken = await resolveAuthToken(true);
207
+ }
193
208
  if (newToken && `Bearer ${newToken}` !== currentToken) {
194
209
  this.headers["Authorization"] = `Bearer ${newToken}`;
195
210
  return this.request(method, path2, opts);
@@ -1948,6 +1963,598 @@ function registerStorageTools(server, api) {
1948
1963
  );
1949
1964
  }
1950
1965
 
1966
+ // libs/mcp-server/src/tools/functions.ts
1967
+ import { z as z7 } from "zod";
1968
+ function registerFunctionTools(server, api) {
1969
+ server.registerTool(
1970
+ "functions_list",
1971
+ {
1972
+ description: "List all functions for a project",
1973
+ inputSchema: {
1974
+ projectId: z7.string()
1975
+ }
1976
+ },
1977
+ async ({ projectId }) => {
1978
+ try {
1979
+ const result = await api.get(
1980
+ `/projects/${encodeURIComponent(projectId)}/functions`
1981
+ );
1982
+ return {
1983
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1984
+ };
1985
+ } catch (error) {
1986
+ const message = error instanceof Error ? error.message : String(error);
1987
+ return {
1988
+ content: [{ type: "text", text: `Failed to list functions: ${message}` }],
1989
+ isError: true
1990
+ };
1991
+ }
1992
+ }
1993
+ );
1994
+ server.registerTool(
1995
+ "functions_get",
1996
+ {
1997
+ description: "Get details of a specific function",
1998
+ inputSchema: {
1999
+ projectId: z7.string(),
2000
+ functionId: z7.string()
2001
+ }
2002
+ },
2003
+ async ({ projectId, functionId }) => {
2004
+ try {
2005
+ const result = await api.get(
2006
+ `/projects/${encodeURIComponent(projectId)}/functions/${encodeURIComponent(functionId)}`
2007
+ );
2008
+ return {
2009
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2010
+ };
2011
+ } catch (error) {
2012
+ const message = error instanceof Error ? error.message : String(error);
2013
+ return {
2014
+ content: [{ type: "text", text: `Failed to get function: ${message}` }],
2015
+ isError: true
2016
+ };
2017
+ }
2018
+ }
2019
+ );
2020
+ server.registerTool(
2021
+ "functions_create",
2022
+ {
2023
+ description: "Create a new serverless function in a project",
2024
+ inputSchema: {
2025
+ projectId: z7.string(),
2026
+ name: z7.string().min(1).max(255),
2027
+ description: z7.string().max(1e3).optional(),
2028
+ entryPoint: z7.string().max(255).optional(),
2029
+ cronExpression: z7.string().optional(),
2030
+ cronTimezone: z7.string().optional(),
2031
+ timeout: z7.number().int().min(1e3).max(12e4).optional(),
2032
+ memoryLimitMb: z7.number().int().min(16).max(512).optional(),
2033
+ enabled: z7.boolean().optional()
2034
+ }
2035
+ },
2036
+ async ({ projectId, ...body }) => {
2037
+ try {
2038
+ const result = await api.post(
2039
+ `/projects/${encodeURIComponent(projectId)}/functions`,
2040
+ { body }
2041
+ );
2042
+ return {
2043
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2044
+ };
2045
+ } catch (error) {
2046
+ const message = error instanceof Error ? error.message : String(error);
2047
+ return {
2048
+ content: [{ type: "text", text: `Failed to create function: ${message}` }],
2049
+ isError: true
2050
+ };
2051
+ }
2052
+ }
2053
+ );
2054
+ server.registerTool(
2055
+ "functions_update",
2056
+ {
2057
+ description: "Update an existing function",
2058
+ inputSchema: {
2059
+ projectId: z7.string(),
2060
+ functionId: z7.string(),
2061
+ name: z7.string().min(1).max(255).optional(),
2062
+ description: z7.string().max(1e3).optional(),
2063
+ entryPoint: z7.string().max(255).optional(),
2064
+ cronExpression: z7.string().optional(),
2065
+ cronTimezone: z7.string().optional(),
2066
+ timeout: z7.number().int().min(1e3).max(12e4).optional(),
2067
+ memoryLimitMb: z7.number().int().min(16).max(512).optional(),
2068
+ enabled: z7.boolean().optional()
2069
+ }
2070
+ },
2071
+ async ({ projectId, functionId, ...body }) => {
2072
+ try {
2073
+ const result = await api.patch(
2074
+ `/projects/${encodeURIComponent(projectId)}/functions/${encodeURIComponent(functionId)}`,
2075
+ { body }
2076
+ );
2077
+ return {
2078
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2079
+ };
2080
+ } catch (error) {
2081
+ const message = error instanceof Error ? error.message : String(error);
2082
+ return {
2083
+ content: [{ type: "text", text: `Failed to update function: ${message}` }],
2084
+ isError: true
2085
+ };
2086
+ }
2087
+ }
2088
+ );
2089
+ server.registerTool(
2090
+ "functions_delete",
2091
+ {
2092
+ description: "Delete a function and its associated cron job",
2093
+ inputSchema: {
2094
+ projectId: z7.string(),
2095
+ functionId: z7.string()
2096
+ }
2097
+ },
2098
+ async ({ projectId, functionId }) => {
2099
+ try {
2100
+ const result = await api.delete(
2101
+ `/projects/${encodeURIComponent(projectId)}/functions/${encodeURIComponent(functionId)}`
2102
+ );
2103
+ return {
2104
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2105
+ };
2106
+ } catch (error) {
2107
+ const message = error instanceof Error ? error.message : String(error);
2108
+ return {
2109
+ content: [{ type: "text", text: `Failed to delete function: ${message}` }],
2110
+ isError: true
2111
+ };
2112
+ }
2113
+ }
2114
+ );
2115
+ server.registerTool(
2116
+ "functions_get_code",
2117
+ {
2118
+ description: "Get the deployed source code of a function with syntax-highlighted file contents",
2119
+ inputSchema: {
2120
+ projectId: z7.string(),
2121
+ functionId: z7.string()
2122
+ }
2123
+ },
2124
+ async ({ projectId, functionId }) => {
2125
+ try {
2126
+ const result = await api.get(
2127
+ `/projects/${encodeURIComponent(projectId)}/functions/${encodeURIComponent(functionId)}/code`
2128
+ );
2129
+ return {
2130
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2131
+ };
2132
+ } catch (error) {
2133
+ const message = error instanceof Error ? error.message : String(error);
2134
+ return {
2135
+ content: [{ type: "text", text: `Failed to get function code: ${message}` }],
2136
+ isError: true
2137
+ };
2138
+ }
2139
+ }
2140
+ );
2141
+ server.registerTool(
2142
+ "functions_deployments_list",
2143
+ {
2144
+ description: "List all deployments (versions) of a function",
2145
+ inputSchema: {
2146
+ projectId: z7.string(),
2147
+ functionId: z7.string()
2148
+ }
2149
+ },
2150
+ async ({ projectId, functionId }) => {
2151
+ try {
2152
+ const result = await api.get(
2153
+ `/projects/${encodeURIComponent(projectId)}/functions/${encodeURIComponent(functionId)}/deployments`
2154
+ );
2155
+ return {
2156
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2157
+ };
2158
+ } catch (error) {
2159
+ const message = error instanceof Error ? error.message : String(error);
2160
+ return {
2161
+ content: [{ type: "text", text: `Failed to list deployments: ${message}` }],
2162
+ isError: true
2163
+ };
2164
+ }
2165
+ }
2166
+ );
2167
+ server.registerTool(
2168
+ "functions_trigger",
2169
+ {
2170
+ description: "Manually trigger a function execution",
2171
+ inputSchema: {
2172
+ projectId: z7.string(),
2173
+ functionId: z7.string()
2174
+ }
2175
+ },
2176
+ async ({ projectId, functionId }) => {
2177
+ try {
2178
+ const result = await api.post(
2179
+ `/projects/${encodeURIComponent(projectId)}/functions/${encodeURIComponent(functionId)}/trigger`
2180
+ );
2181
+ return {
2182
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2183
+ };
2184
+ } catch (error) {
2185
+ const message = error instanceof Error ? error.message : String(error);
2186
+ return {
2187
+ content: [{ type: "text", text: `Failed to trigger function: ${message}` }],
2188
+ isError: true
2189
+ };
2190
+ }
2191
+ }
2192
+ );
2193
+ server.registerTool(
2194
+ "functions_executions_list",
2195
+ {
2196
+ description: "List recent executions of a function with status, duration, and logs",
2197
+ inputSchema: {
2198
+ projectId: z7.string(),
2199
+ functionId: z7.string(),
2200
+ limit: z7.number().int().min(1).max(100).optional(),
2201
+ offset: z7.number().int().min(0).optional()
2202
+ }
2203
+ },
2204
+ async ({ projectId, functionId, limit, offset }) => {
2205
+ try {
2206
+ const result = await api.get(
2207
+ `/projects/${encodeURIComponent(projectId)}/functions/${encodeURIComponent(functionId)}/executions`,
2208
+ { params: { limit, offset } }
2209
+ );
2210
+ return {
2211
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2212
+ };
2213
+ } catch (error) {
2214
+ const message = error instanceof Error ? error.message : String(error);
2215
+ return {
2216
+ content: [{ type: "text", text: `Failed to list executions: ${message}` }],
2217
+ isError: true
2218
+ };
2219
+ }
2220
+ }
2221
+ );
2222
+ server.registerTool(
2223
+ "functions_enable_webhook",
2224
+ {
2225
+ description: "Enable webhook trigger for a function. Returns a one-time webhook secret.",
2226
+ inputSchema: {
2227
+ projectId: z7.string(),
2228
+ functionId: z7.string()
2229
+ }
2230
+ },
2231
+ async ({ projectId, functionId }) => {
2232
+ try {
2233
+ const result = await api.post(
2234
+ `/projects/${encodeURIComponent(projectId)}/functions/${encodeURIComponent(functionId)}/webhook/enable`
2235
+ );
2236
+ return {
2237
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2238
+ };
2239
+ } catch (error) {
2240
+ const message = error instanceof Error ? error.message : String(error);
2241
+ return {
2242
+ content: [{ type: "text", text: `Failed to enable webhook: ${message}` }],
2243
+ isError: true
2244
+ };
2245
+ }
2246
+ }
2247
+ );
2248
+ server.registerTool(
2249
+ "functions_regenerate_webhook_secret",
2250
+ {
2251
+ description: "Regenerate the webhook secret for a function. Old secret stops working immediately.",
2252
+ inputSchema: {
2253
+ projectId: z7.string(),
2254
+ functionId: z7.string()
2255
+ }
2256
+ },
2257
+ async ({ projectId, functionId }) => {
2258
+ try {
2259
+ const result = await api.post(
2260
+ `/projects/${encodeURIComponent(projectId)}/functions/${encodeURIComponent(functionId)}/webhook/regenerate-secret`
2261
+ );
2262
+ return {
2263
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2264
+ };
2265
+ } catch (error) {
2266
+ const message = error instanceof Error ? error.message : String(error);
2267
+ return {
2268
+ content: [{ type: "text", text: `Failed to regenerate webhook secret: ${message}` }],
2269
+ isError: true
2270
+ };
2271
+ }
2272
+ }
2273
+ );
2274
+ server.registerTool(
2275
+ "functions_disable_webhook",
2276
+ {
2277
+ description: "Disable webhook trigger for a function. The webhook secret is deleted.",
2278
+ inputSchema: {
2279
+ projectId: z7.string(),
2280
+ functionId: z7.string()
2281
+ }
2282
+ },
2283
+ async ({ projectId, functionId }) => {
2284
+ try {
2285
+ const result = await api.post(
2286
+ `/projects/${encodeURIComponent(projectId)}/functions/${encodeURIComponent(functionId)}/webhook/disable`
2287
+ );
2288
+ return {
2289
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2290
+ };
2291
+ } catch (error) {
2292
+ const message = error instanceof Error ? error.message : String(error);
2293
+ return {
2294
+ content: [{ type: "text", text: `Failed to disable webhook: ${message}` }],
2295
+ isError: true
2296
+ };
2297
+ }
2298
+ }
2299
+ );
2300
+ server.registerTool(
2301
+ "functions_get_env_vars",
2302
+ {
2303
+ description: "Get environment variables for a function (decrypted). Use these to store API keys and secrets.",
2304
+ inputSchema: {
2305
+ projectId: z7.string(),
2306
+ functionId: z7.string()
2307
+ }
2308
+ },
2309
+ async ({ projectId, functionId }) => {
2310
+ try {
2311
+ const result = await api.get(
2312
+ `/projects/${encodeURIComponent(projectId)}/functions/${encodeURIComponent(functionId)}/env`
2313
+ );
2314
+ return {
2315
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2316
+ };
2317
+ } catch (error) {
2318
+ const message = error instanceof Error ? error.message : String(error);
2319
+ return {
2320
+ content: [{ type: "text", text: `Failed to get env vars: ${message}` }],
2321
+ isError: true
2322
+ };
2323
+ }
2324
+ }
2325
+ );
2326
+ server.registerTool(
2327
+ "functions_set_env_vars",
2328
+ {
2329
+ description: "Set environment variables for a function. Merges with existing vars. Values are encrypted at rest.",
2330
+ inputSchema: {
2331
+ projectId: z7.string(),
2332
+ functionId: z7.string(),
2333
+ variables: z7.record(z7.string(), z7.string())
2334
+ }
2335
+ },
2336
+ async ({ projectId, functionId, variables }) => {
2337
+ try {
2338
+ const existing = await api.get(
2339
+ `/projects/${encodeURIComponent(projectId)}/functions/${encodeURIComponent(functionId)}/env`
2340
+ );
2341
+ const merged = { ...existing, ...variables };
2342
+ await api.patch(
2343
+ `/projects/${encodeURIComponent(projectId)}/functions/${encodeURIComponent(functionId)}`,
2344
+ { body: { environmentVariables: merged } }
2345
+ );
2346
+ return {
2347
+ content: [{ type: "text", text: JSON.stringify({ success: true, keys: Object.keys(merged) }, null, 2) }]
2348
+ };
2349
+ } catch (error) {
2350
+ const message = error instanceof Error ? error.message : String(error);
2351
+ return {
2352
+ content: [{ type: "text", text: `Failed to set env vars: ${message}` }],
2353
+ isError: true
2354
+ };
2355
+ }
2356
+ }
2357
+ );
2358
+ server.registerTool(
2359
+ "functions_kv_list",
2360
+ {
2361
+ description: "List KV store keys for a function",
2362
+ inputSchema: {
2363
+ projectId: z7.string(),
2364
+ functionId: z7.string()
2365
+ }
2366
+ },
2367
+ async ({ projectId, functionId }) => {
2368
+ try {
2369
+ const result = await api.get(
2370
+ `/projects/${encodeURIComponent(projectId)}/functions/${encodeURIComponent(functionId)}/kv`
2371
+ );
2372
+ return {
2373
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2374
+ };
2375
+ } catch (error) {
2376
+ const message = error instanceof Error ? error.message : String(error);
2377
+ return {
2378
+ content: [{ type: "text", text: `Failed to list KV keys: ${message}` }],
2379
+ isError: true
2380
+ };
2381
+ }
2382
+ }
2383
+ );
2384
+ }
2385
+
2386
+ // libs/mcp-server/src/tools/emailTemplates.ts
2387
+ import { z as z8 } from "zod";
2388
+ function registerEmailTemplateTools(server, api) {
2389
+ server.registerTool(
2390
+ "email_templates_list",
2391
+ {
2392
+ description: "List all email templates for a project (includes system defaults)",
2393
+ inputSchema: {
2394
+ projectId: z8.string()
2395
+ }
2396
+ },
2397
+ async ({ projectId }) => {
2398
+ try {
2399
+ const result = await api.get(`/projects/${encodeURIComponent(projectId)}/email-templates`);
2400
+ return {
2401
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2402
+ };
2403
+ } catch (error) {
2404
+ const message = error instanceof Error ? error.message : String(error);
2405
+ return {
2406
+ content: [{ type: "text", text: `Failed to list email templates: ${message}` }],
2407
+ isError: true
2408
+ };
2409
+ }
2410
+ }
2411
+ );
2412
+ server.registerTool(
2413
+ "email_templates_get",
2414
+ {
2415
+ description: "Get a single email template by ID",
2416
+ inputSchema: {
2417
+ id: z8.string()
2418
+ }
2419
+ },
2420
+ async ({ id }) => {
2421
+ try {
2422
+ const result = await api.get(`/email-templates/${encodeURIComponent(id)}`);
2423
+ return {
2424
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2425
+ };
2426
+ } catch (error) {
2427
+ const message = error instanceof Error ? error.message : String(error);
2428
+ return {
2429
+ content: [{ type: "text", text: `Failed to get email template: ${message}` }],
2430
+ isError: true
2431
+ };
2432
+ }
2433
+ }
2434
+ );
2435
+ server.registerTool(
2436
+ "email_templates_create",
2437
+ {
2438
+ description: 'Create a new project-specific email template. The type identifies the template purpose (e.g. "verify-email", "welcome", "invoice").',
2439
+ inputSchema: {
2440
+ projectId: z8.string(),
2441
+ type: z8.string(),
2442
+ name: z8.string(),
2443
+ subject: z8.string(),
2444
+ mjmlSource: z8.string(),
2445
+ variables: z8.array(z8.string()).optional(),
2446
+ isActive: z8.boolean().optional()
2447
+ }
2448
+ },
2449
+ async ({ projectId, type, name, subject, mjmlSource, variables, isActive }) => {
2450
+ try {
2451
+ const body = { projectId, type, name, subject, mjmlSource };
2452
+ if (variables !== void 0) body.variables = variables;
2453
+ if (isActive !== void 0) body.isActive = isActive;
2454
+ const result = await api.post(`/projects/${encodeURIComponent(projectId)}/email-templates`, { body });
2455
+ return {
2456
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2457
+ };
2458
+ } catch (error) {
2459
+ const message = error instanceof Error ? error.message : String(error);
2460
+ return {
2461
+ content: [{ type: "text", text: `Failed to create email template: ${message}` }],
2462
+ isError: true
2463
+ };
2464
+ }
2465
+ }
2466
+ );
2467
+ server.registerTool(
2468
+ "email_templates_update",
2469
+ {
2470
+ description: "Update an existing email template",
2471
+ inputSchema: {
2472
+ id: z8.string(),
2473
+ name: z8.string().optional(),
2474
+ subject: z8.string().optional(),
2475
+ mjmlSource: z8.string().optional(),
2476
+ variables: z8.array(z8.string()).optional(),
2477
+ isActive: z8.boolean().optional()
2478
+ }
2479
+ },
2480
+ async ({ id, name, subject, mjmlSource, variables, isActive }) => {
2481
+ try {
2482
+ const body = {};
2483
+ if (name !== void 0) body.name = name;
2484
+ if (subject !== void 0) body.subject = subject;
2485
+ if (mjmlSource !== void 0) body.mjmlSource = mjmlSource;
2486
+ if (variables !== void 0) body.variables = variables;
2487
+ if (isActive !== void 0) body.isActive = isActive;
2488
+ if (Object.keys(body).length === 0) {
2489
+ return {
2490
+ content: [{ type: "text", text: "At least one field must be provided" }],
2491
+ isError: true
2492
+ };
2493
+ }
2494
+ const result = await api.patch(`/email-templates/${encodeURIComponent(id)}`, { body });
2495
+ return {
2496
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2497
+ };
2498
+ } catch (error) {
2499
+ const message = error instanceof Error ? error.message : String(error);
2500
+ return {
2501
+ content: [{ type: "text", text: `Failed to update email template: ${message}` }],
2502
+ isError: true
2503
+ };
2504
+ }
2505
+ }
2506
+ );
2507
+ server.registerTool(
2508
+ "email_templates_delete",
2509
+ {
2510
+ description: "Delete an email template. System default templates cannot be deleted.",
2511
+ inputSchema: {
2512
+ id: z8.string()
2513
+ }
2514
+ },
2515
+ async ({ id }) => {
2516
+ try {
2517
+ const result = await api.delete(`/email-templates/${encodeURIComponent(id)}`);
2518
+ return {
2519
+ content: [{ type: "text", text: result ? JSON.stringify(result, null, 2) : "Email template deleted" }]
2520
+ };
2521
+ } catch (error) {
2522
+ const message = error instanceof Error ? error.message : String(error);
2523
+ return {
2524
+ content: [{ type: "text", text: `Failed to delete email template: ${message}` }],
2525
+ isError: true
2526
+ };
2527
+ }
2528
+ }
2529
+ );
2530
+ server.registerTool(
2531
+ "email_templates_preview",
2532
+ {
2533
+ description: "Preview a compiled email template. Renders MJML to HTML and applies Handlebars variables.",
2534
+ inputSchema: {
2535
+ mjmlSource: z8.string(),
2536
+ variables: z8.record(z8.string(), z8.unknown()).optional()
2537
+ }
2538
+ },
2539
+ async ({ mjmlSource, variables }) => {
2540
+ try {
2541
+ const body = { mjmlSource };
2542
+ if (variables !== void 0) body.variables = variables;
2543
+ const result = await api.post("/email-templates/preview", { body });
2544
+ return {
2545
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2546
+ };
2547
+ } catch (error) {
2548
+ const message = error instanceof Error ? error.message : String(error);
2549
+ return {
2550
+ content: [{ type: "text", text: `Failed to preview email template: ${message}` }],
2551
+ isError: true
2552
+ };
2553
+ }
2554
+ }
2555
+ );
2556
+ }
2557
+
1951
2558
  // libs/mcp-server/src/tools/index.ts
1952
2559
  function registerAllTools(server, api) {
1953
2560
  registerAuthTools(server, api);
@@ -1956,6 +2563,313 @@ function registerAllTools(server, api) {
1956
2563
  registerDatabaseTools(server, api);
1957
2564
  registerHostingTools(server, api);
1958
2565
  registerStorageTools(server, api);
2566
+ registerFunctionTools(server, api);
2567
+ registerEmailTemplateTools(server, api);
2568
+ }
2569
+
2570
+ // libs/mcp-server/src/prompts/database.ts
2571
+ import { z as z9 } from "zod";
2572
+ function registerDatabasePrompts(server) {
2573
+ server.registerPrompt(
2574
+ "setup-collection",
2575
+ {
2576
+ title: "Setup Database Collection",
2577
+ description: "Guide for creating a new database collection with proper security rules. Collections MUST have rules defined before data can be inserted.",
2578
+ argsSchema: {
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)")
2582
+ }
2583
+ },
2584
+ async ({ projectId, collectionName, access }) => {
2585
+ const accessLevel = access || "admin-only";
2586
+ const rules = {
2587
+ "public-read": {
2588
+ ".create": "true",
2589
+ ".read": "true",
2590
+ ".update": "false",
2591
+ ".delete": "auth.role === 'owner' || auth.role === 'admin'"
2592
+ },
2593
+ authenticated: {
2594
+ ".create": "!!auth.uid",
2595
+ ".read": "!!auth.uid",
2596
+ ".update": "!!auth.uid",
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
+ };
2638
+ }
2639
+ );
2640
+ server.registerPrompt(
2641
+ "database-workflow",
2642
+ {
2643
+ title: "Database Workflow Guide",
2644
+ description: "Explains how the Spacelr database system works: rules-first approach, collection lifecycle, and security model.",
2645
+ argsSchema: {
2646
+ projectId: z9.string().describe("Project ID")
2647
+ }
2648
+ },
2649
+ async ({ projectId }) => {
2650
+ return {
2651
+ messages: [
2652
+ {
2653
+ role: "user",
2654
+ content: {
2655
+ type: "text",
2656
+ text: [
2657
+ `Guide me through the Spacelr database system for project ${projectId}.`,
2658
+ "",
2659
+ "CORE CONCEPTS:",
2660
+ "",
2661
+ "1. RULES-FIRST: Collections are defined through security rules.",
2662
+ " - Use database_rules_get to see current rules",
2663
+ " - Use database_rules_set to add/modify collection rules",
2664
+ " - Setting rules automatically creates the collection definition",
2665
+ " - Without rules, all operations are denied by the $other catch-all",
2666
+ "",
2667
+ "2. SECURITY RULES SYNTAX:",
2668
+ ' - ".create": Expression that must evaluate to true for inserts',
2669
+ ' - ".read": Expression for reads/queries',
2670
+ ' - ".update": Expression for document updates',
2671
+ ' - ".delete": Expression for document deletion',
2672
+ ' - ".validate": Per-field validation expressions',
2673
+ ' - ".schema": Field type declarations',
2674
+ " Available context: auth.uid, auth.role, newData (for validation)",
2675
+ "",
2676
+ "3. WORKFLOW:",
2677
+ " a) Define rules for the collection",
2678
+ " b) Optionally create indexes (database_indexes_create)",
2679
+ " c) Insert/query documents",
2680
+ "",
2681
+ "4. DATA ISOLATION:",
2682
+ " - Each project has its own database (project_{projectId})",
2683
+ " - Collections are scoped to the project automatically",
2684
+ " - No cross-project data access is possible"
2685
+ ].join("\n")
2686
+ }
2687
+ }
2688
+ ]
2689
+ };
2690
+ }
2691
+ );
2692
+ }
2693
+
2694
+ // libs/mcp-server/src/prompts/functions.ts
2695
+ import { z as z10 } from "zod";
2696
+ function registerFunctionPrompts(server) {
2697
+ server.registerPrompt(
2698
+ "deploy-function",
2699
+ {
2700
+ title: "Deploy a Serverless Function",
2701
+ description: "Step-by-step guide for creating, deploying, and testing a serverless function. Covers the full lifecycle from creation to execution.",
2702
+ argsSchema: {
2703
+ projectId: z10.string().describe("Project ID"),
2704
+ name: z10.string().describe("Function name"),
2705
+ useCase: z10.string().optional().describe('What the function should do (e.g. "fetch data from API and store in DB")')
2706
+ }
2707
+ },
2708
+ async ({ projectId, name, useCase }) => {
2709
+ return {
2710
+ messages: [
2711
+ {
2712
+ role: "user",
2713
+ content: {
2714
+ type: "text",
2715
+ text: [
2716
+ `Deploy a serverless function "${name}" in project ${projectId}.`,
2717
+ useCase ? `Use case: ${useCase}` : "",
2718
+ "",
2719
+ "DEPLOYMENT WORKFLOW:",
2720
+ "",
2721
+ "1. CREATE the function:",
2722
+ " Use functions_create with name, entryPoint (default: index.js),",
2723
+ " timeout (ms, default: 30000), memoryLimitMb (default: 128)",
2724
+ " Optional: cronExpression for scheduled execution",
2725
+ "",
2726
+ "2. WRITE the code:",
2727
+ " Create an index.js file. Available APIs inside the sandbox:",
2728
+ "",
2729
+ " - console.log/warn/error/info() \u2014 captured to execution logs",
2730
+ " - await fetch(url, options) \u2014 HTTP client (10s timeout, 5MB limit)",
2731
+ " Response: { status, headers, body } where body is a STRING",
2732
+ " Use JSON.parse(response.body) for JSON APIs",
2733
+ " - env.get(key) \u2014 read environment variables (API keys, secrets \u2014 set via Console UI or CLI)",
2734
+ " - await kv.get/set/delete/list() \u2014 Redis KV store (256KB values, 1000 keys)",
2735
+ " - await spacelr.db.collection(name).find/insertOne/insertMany()",
2736
+ " - await spacelr.storage.list/getInfo/getDownloadUrl()",
2737
+ " - await spacelr.email.send/sendRaw() \u2014 send template or raw emails",
2738
+ " - await spacelr.notifications.send/sendMany() \u2014 push notifications",
2739
+ "",
2740
+ " IMPORTANT: Top-level await is supported. No imports/require available.",
2741
+ "",
2742
+ "3. IF USING spacelr.db:",
2743
+ " Database rules MUST be set BEFORE the function writes to a collection.",
2744
+ " Use the setup-collection prompt or manually set rules via database_rules_set.",
2745
+ ' Without rules, inserts will fail with "Rule denied".',
2746
+ "",
2747
+ "4. DEPLOY the code:",
2748
+ " Zip the file(s) and upload via the CLI:",
2749
+ " spacelr functions deploy ./my-function --name " + name,
2750
+ " Or use the admin API: POST /projects/{projectId}/functions/{id}/deploy",
2751
+ "",
2752
+ "5. TRIGGER:",
2753
+ " Functions support multiple trigger types:",
2754
+ "",
2755
+ " a) Manual: Use functions_trigger to execute on demand.",
2756
+ " b) Cron: Set cronExpression when creating/updating the function.",
2757
+ " c) Webhook: Enable with functions_enable_webhook to get a secret,",
2758
+ " then POST to /api/v1/functions/{projectId}/{functionId}/invoke",
2759
+ " with the X-Webhook-Secret header. Use functions_regenerate_webhook_secret",
2760
+ " to rotate the secret (old secret stops working immediately).",
2761
+ " d) Event: Configure event triggers to run the function in response",
2762
+ " to platform events (e.g. database changes, storage uploads).",
2763
+ "",
2764
+ " Check results with functions_executions_list.",
2765
+ "",
2766
+ "6. VERIFY:",
2767
+ " Check execution status, logs, and duration.",
2768
+ " Use functions_get_code to review the deployed source."
2769
+ ].join("\n")
2770
+ }
2771
+ }
2772
+ ]
2773
+ };
2774
+ }
2775
+ );
2776
+ server.registerPrompt(
2777
+ "function-sandbox-reference",
2778
+ {
2779
+ title: "Function Sandbox API Reference",
2780
+ description: "Complete reference for all APIs available inside a serverless function sandbox.",
2781
+ argsSchema: {}
2782
+ },
2783
+ async () => {
2784
+ return {
2785
+ messages: [
2786
+ {
2787
+ role: "user",
2788
+ content: {
2789
+ type: "text",
2790
+ text: [
2791
+ "Show me the complete API reference for the Spacelr function sandbox.",
2792
+ "",
2793
+ "\u2550\u2550\u2550 CONSOLE \u2550\u2550\u2550",
2794
+ "console.log(...args) \u2014 log level",
2795
+ "console.info(...args) \u2014 info level",
2796
+ "console.warn(...args) \u2014 warn level",
2797
+ "console.error(...args) \u2014 error level",
2798
+ "All logs are captured and stored in the execution record.",
2799
+ "Log buffer is capped at 1MB.",
2800
+ "",
2801
+ "\u2550\u2550\u2550 FETCH \u2550\u2550\u2550",
2802
+ "const response = await fetch(url, options?)",
2803
+ " options: { method, headers, body }",
2804
+ " returns: { status: number, headers: Record<string,string>, body: string }",
2805
+ " NOTE: body is always a string \u2014 use JSON.parse(response.body) for JSON",
2806
+ " Limits: 10s timeout, 5MB response, no private/internal URLs (SSRF blocked)",
2807
+ "",
2808
+ "\u2550\u2550\u2550 ENV (Environment Variables) \u2550\u2550\u2550",
2809
+ "env.get(key) \u2192 string | undefined",
2810
+ "Read-only access to function environment variables.",
2811
+ "Use env vars to store API keys, secrets, and configuration \u2014 never hardcode them in function code.",
2812
+ "Set via Console UI (Function Detail \u2192 Environment Variables) or CLI (spacelr functions env set).",
2813
+ "Values are encrypted at rest and only decrypted when the function executes.",
2814
+ "",
2815
+ "\u2550\u2550\u2550 KV STORE \u2550\u2550\u2550",
2816
+ "await kv.get(key) \u2192 string | null",
2817
+ "await kv.set(key, value, ttlSeconds?) \u2192 void",
2818
+ "await kv.delete(key) \u2192 boolean",
2819
+ "await kv.list(prefix?) \u2192 string[]",
2820
+ "Backed by Redis. Scoped per function.",
2821
+ "Limits: 256KB per value, 1000 keys per function, 30-day default TTL.",
2822
+ "",
2823
+ "\u2550\u2550\u2550 DATABASE \u2550\u2550\u2550",
2824
+ "const docs = await spacelr.db.collection(name).find(filter?, options?)",
2825
+ " options: { sort, limit, offset }",
2826
+ "await spacelr.db.collection(name).insertOne(doc)",
2827
+ "await spacelr.db.collection(name).insertMany(docs)",
2828
+ "Scoped to the function's project. Requires database rules to be set first.",
2829
+ "",
2830
+ "\u2550\u2550\u2550 STORAGE \u2550\u2550\u2550",
2831
+ "await spacelr.storage.list(options?) \u2192 FileInfo[]",
2832
+ "await spacelr.storage.getInfo(fileId) \u2192 FileInfo",
2833
+ "await spacelr.storage.getDownloadUrl(fileId) \u2192 string",
2834
+ "Scoped to the function's project.",
2835
+ "",
2836
+ "\u2550\u2550\u2550 EMAIL \u2550\u2550\u2550",
2837
+ "await spacelr.email.send({ to, template, variables })",
2838
+ " Sends a template-based email using project templates (MJML + Handlebars).",
2839
+ " template: template name (must exist in the project)",
2840
+ " variables: Record<string, string> of template variables",
2841
+ " Returns: { messageId }",
2842
+ "await spacelr.email.sendRaw({ to, subject, html })",
2843
+ " Sends a raw HTML email directly. Max 500KB HTML.",
2844
+ " Returns: { messageId }",
2845
+ "Rate limit: 100 emails/hour per project (configurable via FUNCTION_EMAIL_HOURLY_LIMIT).",
2846
+ "",
2847
+ "\u2550\u2550\u2550 NOTIFICATIONS \u2550\u2550\u2550",
2848
+ "await spacelr.notifications.send({ userId, title, body, data? })",
2849
+ " Sends a push notification to a specific user in the project.",
2850
+ "await spacelr.notifications.sendMany({ userIds, title, body, data? })",
2851
+ " Sends to multiple users. Deduplicates userIds automatically.",
2852
+ "Rate limit: 500 notifications/hour per project (configurable via FUNCTION_NOTIFICATION_HOURLY_LIMIT). Scoped to the function's project.",
2853
+ "",
2854
+ "\u2550\u2550\u2550 RESOURCE LIMITS \u2550\u2550\u2550",
2855
+ "Execution timeout: 30s default, 120s max",
2856
+ "Memory: 128MB default, 512MB max",
2857
+ "Code bundle: 10MB max (zip)",
2858
+ "Concurrent executions: 10 per project",
2859
+ "CPU time: limited by watchdog (equals timeout)"
2860
+ ].join("\n")
2861
+ }
2862
+ }
2863
+ ]
2864
+ };
2865
+ }
2866
+ );
2867
+ }
2868
+
2869
+ // libs/mcp-server/src/prompts/index.ts
2870
+ function registerAllPrompts(server) {
2871
+ registerDatabasePrompts(server);
2872
+ registerFunctionPrompts(server);
1959
2873
  }
1960
2874
 
1961
2875
  // libs/mcp-server/src/index.ts
@@ -1967,6 +2881,7 @@ async function main() {
1967
2881
  version: "0.1.0"
1968
2882
  });
1969
2883
  registerAllTools(server, api);
2884
+ registerAllPrompts(server);
1970
2885
  const transport = new StdioServerTransport();
1971
2886
  await server.connect(transport);
1972
2887
  const shutdown = async () => {