@spacelr/mcp 0.0.4 → 0.0.6

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);
@@ -2282,6 +2297,64 @@ function registerFunctionTools(server, api) {
2282
2297
  }
2283
2298
  }
2284
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
+ );
2285
2358
  server.registerTool(
2286
2359
  "functions_kv_list",
2287
2360
  {
@@ -2310,207 +2383,1337 @@ function registerFunctionTools(server, api) {
2310
2383
  );
2311
2384
  }
2312
2385
 
2313
- // libs/mcp-server/src/tools/index.ts
2314
- function registerAllTools(server, api) {
2315
- registerAuthTools(server, api);
2316
- registerProjectTools(server, api);
2317
- registerClientTools(server, api);
2318
- registerDatabaseTools(server, api);
2319
- registerHostingTools(server, api);
2320
- registerStorageTools(server, api);
2321
- registerFunctionTools(server, api);
2322
- }
2323
-
2324
- // libs/mcp-server/src/prompts/database.ts
2386
+ // libs/mcp-server/src/tools/emailTemplates.ts
2325
2387
  import { z as z8 } from "zod";
2326
- function registerDatabasePrompts(server) {
2327
- server.registerPrompt(
2328
- "setup-collection",
2388
+ function registerEmailTemplateTools(server, api) {
2389
+ server.registerTool(
2390
+ "email_templates_list",
2329
2391
  {
2330
- title: "Setup Database Collection",
2331
- description: "Guide for creating a new database collection with proper security rules. Collections MUST have rules defined before data can be inserted.",
2332
- argsSchema: {
2333
- projectId: z8.string().describe("Project ID"),
2334
- collectionName: z8.string().describe("Name of the collection to create"),
2335
- access: z8.enum(["public-read", "authenticated", "admin-only"]).optional().describe("Access level (default: admin-only)")
2392
+ description: "List all email templates for a project (includes system defaults)",
2393
+ inputSchema: {
2394
+ projectId: z8.string()
2336
2395
  }
2337
2396
  },
2338
- async ({ projectId, collectionName, access }) => {
2339
- const accessLevel = access || "admin-only";
2340
- const rules = {
2341
- "public-read": {
2342
- ".create": "true",
2343
- ".read": "true",
2344
- ".update": "false",
2345
- ".delete": "auth.role === 'owner' || auth.role === 'admin'"
2346
- },
2347
- authenticated: {
2348
- ".create": "!!auth.uid",
2349
- ".read": "!!auth.uid",
2350
- ".update": "!!auth.uid",
2351
- ".delete": "auth.role === 'owner' || auth.role === 'admin'"
2352
- },
2353
- "admin-only": {
2354
- ".create": "auth.role === 'owner' || auth.role === 'admin'",
2355
- ".read": "auth.role === 'owner' || auth.role === 'admin'",
2356
- ".update": "auth.role === 'owner' || auth.role === 'admin'",
2357
- ".delete": "auth.role === 'owner' || auth.role === 'admin'"
2358
- }
2359
- };
2360
- const collectionRules = rules[accessLevel];
2361
- const rulesJson = JSON.stringify(collectionRules, null, 2);
2362
- return {
2363
- messages: [
2364
- {
2365
- role: "user",
2366
- content: {
2367
- type: "text",
2368
- text: [
2369
- `Set up a new database collection "${collectionName}" in project ${projectId}.`,
2370
- "",
2371
- "IMPORTANT WORKFLOW:",
2372
- "1. First, get the current rules with database_rules_get",
2373
- "2. Add the new collection rules to the existing rules object",
2374
- "3. Set the updated rules with database_rules_set \u2014 this automatically creates the collection",
2375
- '4. Do NOT try to insert documents before the rules are set \u2014 it will fail with "Rule denied"',
2376
- "",
2377
- `Suggested rules for "${collectionName}" (${accessLevel}):`,
2378
- rulesJson,
2379
- "",
2380
- "The $other catch-all rule should deny access to undefined collections:",
2381
- ' "$other": { ".read": "false", ".write": "false" }',
2382
- "",
2383
- "After rules are set, the collection is ready for use via:",
2384
- "- database_documents_insert",
2385
- "- database_documents_find",
2386
- '- Serverless functions using spacelr.db.collection("' + collectionName + '")'
2387
- ].join("\n")
2388
- }
2389
- }
2390
- ]
2391
- };
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
+ }
2392
2410
  }
2393
2411
  );
2394
- server.registerPrompt(
2395
- "database-workflow",
2412
+ server.registerTool(
2413
+ "email_templates_get",
2396
2414
  {
2397
- title: "Database Workflow Guide",
2398
- description: "Explains how the Spacelr database system works: rules-first approach, collection lifecycle, and security model.",
2399
- argsSchema: {
2400
- projectId: z8.string().describe("Project ID")
2415
+ description: "Get a single email template by ID",
2416
+ inputSchema: {
2417
+ id: z8.string()
2401
2418
  }
2402
2419
  },
2403
- async ({ projectId }) => {
2404
- return {
2405
- messages: [
2406
- {
2407
- role: "user",
2408
- content: {
2409
- type: "text",
2410
- text: [
2411
- `Guide me through the Spacelr database system for project ${projectId}.`,
2412
- "",
2413
- "CORE CONCEPTS:",
2414
- "",
2415
- "1. RULES-FIRST: Collections are defined through security rules.",
2416
- " - Use database_rules_get to see current rules",
2417
- " - Use database_rules_set to add/modify collection rules",
2418
- " - Setting rules automatically creates the collection definition",
2419
- " - Without rules, all operations are denied by the $other catch-all",
2420
- "",
2421
- "2. SECURITY RULES SYNTAX:",
2422
- ' - ".create": Expression that must evaluate to true for inserts',
2423
- ' - ".read": Expression for reads/queries',
2424
- ' - ".update": Expression for document updates',
2425
- ' - ".delete": Expression for document deletion',
2426
- ' - ".validate": Per-field validation expressions',
2427
- ' - ".schema": Field type declarations',
2428
- " Available context: auth.uid, auth.role, newData (for validation)",
2429
- "",
2430
- "3. WORKFLOW:",
2431
- " a) Define rules for the collection",
2432
- " b) Optionally create indexes (database_indexes_create)",
2433
- " c) Insert/query documents",
2434
- "",
2435
- "4. DATA ISOLATION:",
2436
- " - Each project has its own database (project_{projectId})",
2437
- " - Collections are scoped to the project automatically",
2438
- " - No cross-project data access is possible"
2439
- ].join("\n")
2440
- }
2441
- }
2442
- ]
2443
- };
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
+ }
2444
2433
  }
2445
2434
  );
2446
- }
2447
-
2448
- // libs/mcp-server/src/prompts/functions.ts
2449
- import { z as z9 } from "zod";
2450
- function registerFunctionPrompts(server) {
2451
- server.registerPrompt(
2452
- "deploy-function",
2435
+ server.registerTool(
2436
+ "email_templates_create",
2453
2437
  {
2454
- title: "Deploy a Serverless Function",
2455
- description: "Step-by-step guide for creating, deploying, and testing a serverless function. Covers the full lifecycle from creation to execution.",
2456
- argsSchema: {
2457
- projectId: z9.string().describe("Project ID"),
2458
- name: z9.string().describe("Function name"),
2459
- useCase: z9.string().optional().describe('What the function should do (e.g. "fetch data from API and store in DB")')
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()
2460
2447
  }
2461
2448
  },
2462
- async ({ projectId, name, useCase }) => {
2463
- return {
2464
- messages: [
2465
- {
2466
- role: "user",
2467
- content: {
2468
- type: "text",
2469
- text: [
2470
- `Deploy a serverless function "${name}" in project ${projectId}.`,
2471
- useCase ? `Use case: ${useCase}` : "",
2472
- "",
2473
- "DEPLOYMENT WORKFLOW:",
2474
- "",
2475
- "1. CREATE the function:",
2476
- " Use functions_create with name, entryPoint (default: index.js),",
2477
- " timeout (ms, default: 30000), memoryLimitMb (default: 128)",
2478
- " Optional: cronExpression for scheduled execution",
2479
- "",
2480
- "2. WRITE the code:",
2481
- " Create an index.js file. Available APIs inside the sandbox:",
2482
- "",
2483
- " - console.log/warn/error/info() \u2014 captured to execution logs",
2484
- " - await fetch(url, options) \u2014 HTTP client (10s timeout, 5MB limit)",
2485
- " Response: { status, headers, body } where body is a STRING",
2486
- " Use JSON.parse(response.body) for JSON APIs",
2487
- " - env.get(key) \u2014 read environment variables",
2488
- " - await kv.get/set/delete/list() \u2014 Redis KV store (256KB values, 1000 keys)",
2489
- " - await spacelr.db.collection(name).find/insertOne/insertMany()",
2490
- " - await spacelr.storage.list/getInfo/getDownloadUrl()",
2491
- "",
2492
- " IMPORTANT: Top-level await is supported. No imports/require available.",
2493
- "",
2494
- "3. IF USING spacelr.db:",
2495
- " Database rules MUST be set BEFORE the function writes to a collection.",
2496
- " Use the setup-collection prompt or manually set rules via database_rules_set.",
2497
- ' Without rules, inserts will fail with "Rule denied".',
2498
- "",
2499
- "4. DEPLOY the code:",
2500
- " Zip the file(s) and upload via the CLI:",
2501
- " spacelr functions deploy ./my-function --name " + name,
2502
- " Or use the admin API: POST /projects/{projectId}/functions/{id}/deploy",
2503
- "",
2504
- "5. TRIGGER:",
2505
- " Functions support multiple trigger types:",
2506
- "",
2507
- " a) Manual: Use functions_trigger to execute on demand.",
2508
- " b) Cron: Set cronExpression when creating/updating the function.",
2509
- " c) Webhook: Enable with functions_enable_webhook to get a secret,",
2510
- " then POST to /api/v1/functions/{projectId}/{functionId}/invoke",
2511
- " with the X-Webhook-Secret header. Use functions_regenerate_webhook_secret",
2512
- " to rotate the secret (old secret stops working immediately).",
2513
- " d) Event: Configure event triggers to run the function in response",
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
+
2558
+ // libs/mcp-server/src/tools/cronJobs.ts
2559
+ import { z as z9 } from "zod";
2560
+ function registerCronJobTools(server, api) {
2561
+ server.registerTool(
2562
+ "cron_jobs_list",
2563
+ {
2564
+ description: "List all cron jobs for a project",
2565
+ inputSchema: {
2566
+ projectId: z9.string()
2567
+ }
2568
+ },
2569
+ async ({ projectId }) => {
2570
+ try {
2571
+ const result = await api.get(`/projects/${encodeURIComponent(projectId)}/cron-jobs`);
2572
+ return {
2573
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2574
+ };
2575
+ } catch (error) {
2576
+ const message = error instanceof Error ? error.message : String(error);
2577
+ return {
2578
+ content: [{ type: "text", text: `Failed to list cron jobs: ${message}` }],
2579
+ isError: true
2580
+ };
2581
+ }
2582
+ }
2583
+ );
2584
+ server.registerTool(
2585
+ "cron_jobs_get",
2586
+ {
2587
+ description: "Get a single cron job by ID",
2588
+ inputSchema: {
2589
+ projectId: z9.string(),
2590
+ jobId: z9.string()
2591
+ }
2592
+ },
2593
+ async ({ projectId, jobId }) => {
2594
+ try {
2595
+ const result = await api.get(`/projects/${encodeURIComponent(projectId)}/cron-jobs/${encodeURIComponent(jobId)}`);
2596
+ return {
2597
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2598
+ };
2599
+ } catch (error) {
2600
+ const message = error instanceof Error ? error.message : String(error);
2601
+ return {
2602
+ content: [{ type: "text", text: `Failed to get cron job: ${message}` }],
2603
+ isError: true
2604
+ };
2605
+ }
2606
+ }
2607
+ );
2608
+ server.registerTool(
2609
+ "cron_jobs_create",
2610
+ {
2611
+ 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 * * *").',
2612
+ inputSchema: {
2613
+ projectId: z9.string(),
2614
+ name: z9.string(),
2615
+ cronExpression: z9.string().regex(/^(\S+\s){4}\S+$/),
2616
+ timezone: z9.string().optional(),
2617
+ type: z9.enum(["webhook", "function"]).optional(),
2618
+ functionId: z9.string().optional(),
2619
+ url: z9.string().url().optional(),
2620
+ secret: z9.string().optional(),
2621
+ payload: z9.record(z9.string(), z9.unknown()).optional(),
2622
+ retryAttempts: z9.number().int().min(0).max(10).optional(),
2623
+ timeout: z9.number().int().min(1e3).max(12e4).optional(),
2624
+ enabled: z9.boolean().optional()
2625
+ }
2626
+ },
2627
+ async ({ projectId, name, cronExpression, timezone, type, functionId, url, secret, payload, retryAttempts, timeout, enabled }) => {
2628
+ try {
2629
+ const body = { name, cronExpression };
2630
+ if (timezone !== void 0) body.timezone = timezone;
2631
+ if (type !== void 0) body.type = type;
2632
+ if (functionId !== void 0) body.functionId = functionId;
2633
+ if (url !== void 0) body.url = url;
2634
+ if (secret !== void 0) body.secret = secret;
2635
+ if (payload !== void 0) body.payload = payload;
2636
+ if (retryAttempts !== void 0) body.retryAttempts = retryAttempts;
2637
+ if (timeout !== void 0) body.timeout = timeout;
2638
+ if (enabled !== void 0) body.enabled = enabled;
2639
+ const result = await api.post(`/projects/${encodeURIComponent(projectId)}/cron-jobs`, { body });
2640
+ return {
2641
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2642
+ };
2643
+ } catch (error) {
2644
+ const message = error instanceof Error ? error.message : String(error);
2645
+ return {
2646
+ content: [{ type: "text", text: `Failed to create cron job: ${message}` }],
2647
+ isError: true
2648
+ };
2649
+ }
2650
+ }
2651
+ );
2652
+ server.registerTool(
2653
+ "cron_jobs_update",
2654
+ {
2655
+ description: "Update an existing cron job",
2656
+ inputSchema: {
2657
+ projectId: z9.string(),
2658
+ jobId: z9.string(),
2659
+ name: z9.string().optional(),
2660
+ cronExpression: z9.string().regex(/^(\S+\s){4}\S+$/).optional(),
2661
+ timezone: z9.string().optional(),
2662
+ type: z9.enum(["webhook", "function"]).optional(),
2663
+ functionId: z9.string().optional(),
2664
+ url: z9.string().url().optional(),
2665
+ secret: z9.string().optional(),
2666
+ payload: z9.record(z9.string(), z9.unknown()).optional(),
2667
+ retryAttempts: z9.number().int().min(0).max(10).optional(),
2668
+ timeout: z9.number().int().min(1e3).max(12e4).optional(),
2669
+ enabled: z9.boolean().optional()
2670
+ }
2671
+ },
2672
+ async ({ projectId, jobId, name, cronExpression, timezone, type, functionId, url, secret, payload, retryAttempts, timeout, enabled }) => {
2673
+ try {
2674
+ const body = {};
2675
+ if (name !== void 0) body.name = name;
2676
+ if (cronExpression !== void 0) body.cronExpression = cronExpression;
2677
+ if (timezone !== void 0) body.timezone = timezone;
2678
+ if (type !== void 0) body.type = type;
2679
+ if (functionId !== void 0) body.functionId = functionId;
2680
+ if (url !== void 0) body.url = url;
2681
+ if (secret !== void 0) body.secret = secret;
2682
+ if (payload !== void 0) body.payload = payload;
2683
+ if (retryAttempts !== void 0) body.retryAttempts = retryAttempts;
2684
+ if (timeout !== void 0) body.timeout = timeout;
2685
+ if (enabled !== void 0) body.enabled = enabled;
2686
+ if (Object.keys(body).length === 0) {
2687
+ return {
2688
+ content: [{ type: "text", text: "At least one field must be provided" }],
2689
+ isError: true
2690
+ };
2691
+ }
2692
+ const result = await api.patch(`/projects/${encodeURIComponent(projectId)}/cron-jobs/${encodeURIComponent(jobId)}`, { body });
2693
+ return {
2694
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2695
+ };
2696
+ } catch (error) {
2697
+ const message = error instanceof Error ? error.message : String(error);
2698
+ return {
2699
+ content: [{ type: "text", text: `Failed to update cron job: ${message}` }],
2700
+ isError: true
2701
+ };
2702
+ }
2703
+ }
2704
+ );
2705
+ server.registerTool(
2706
+ "cron_jobs_delete",
2707
+ {
2708
+ description: "Delete a cron job",
2709
+ inputSchema: {
2710
+ projectId: z9.string(),
2711
+ jobId: z9.string()
2712
+ }
2713
+ },
2714
+ async ({ projectId, jobId }) => {
2715
+ try {
2716
+ const result = await api.delete(`/projects/${encodeURIComponent(projectId)}/cron-jobs/${encodeURIComponent(jobId)}`);
2717
+ return {
2718
+ content: [{ type: "text", text: result ? JSON.stringify(result, null, 2) : "Cron job deleted" }]
2719
+ };
2720
+ } catch (error) {
2721
+ const message = error instanceof Error ? error.message : String(error);
2722
+ return {
2723
+ content: [{ type: "text", text: `Failed to delete cron job: ${message}` }],
2724
+ isError: true
2725
+ };
2726
+ }
2727
+ }
2728
+ );
2729
+ server.registerTool(
2730
+ "cron_jobs_trigger",
2731
+ {
2732
+ description: "Manually trigger a cron job execution",
2733
+ inputSchema: {
2734
+ projectId: z9.string(),
2735
+ jobId: z9.string()
2736
+ }
2737
+ },
2738
+ async ({ projectId, jobId }) => {
2739
+ try {
2740
+ const result = await api.post(`/projects/${encodeURIComponent(projectId)}/cron-jobs/${encodeURIComponent(jobId)}/trigger`);
2741
+ return {
2742
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2743
+ };
2744
+ } catch (error) {
2745
+ const message = error instanceof Error ? error.message : String(error);
2746
+ return {
2747
+ content: [{ type: "text", text: `Failed to trigger cron job: ${message}` }],
2748
+ isError: true
2749
+ };
2750
+ }
2751
+ }
2752
+ );
2753
+ server.registerTool(
2754
+ "cron_jobs_toggle",
2755
+ {
2756
+ description: "Toggle a cron job enabled/disabled",
2757
+ inputSchema: {
2758
+ projectId: z9.string(),
2759
+ jobId: z9.string(),
2760
+ enabled: z9.boolean()
2761
+ }
2762
+ },
2763
+ async ({ projectId, jobId, enabled }) => {
2764
+ try {
2765
+ const result = await api.post(`/projects/${encodeURIComponent(projectId)}/cron-jobs/${encodeURIComponent(jobId)}/toggle`, { body: { enabled } });
2766
+ return {
2767
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2768
+ };
2769
+ } catch (error) {
2770
+ const message = error instanceof Error ? error.message : String(error);
2771
+ return {
2772
+ content: [{ type: "text", text: `Failed to toggle cron job: ${message}` }],
2773
+ isError: true
2774
+ };
2775
+ }
2776
+ }
2777
+ );
2778
+ server.registerTool(
2779
+ "cron_jobs_executions",
2780
+ {
2781
+ description: "List execution history for a cron job",
2782
+ inputSchema: {
2783
+ projectId: z9.string(),
2784
+ jobId: z9.string(),
2785
+ limit: z9.number().int().min(1).max(100).optional(),
2786
+ offset: z9.number().int().min(0).optional()
2787
+ }
2788
+ },
2789
+ async ({ projectId, jobId, limit, offset }) => {
2790
+ try {
2791
+ const result = await api.get(`/projects/${encodeURIComponent(projectId)}/cron-jobs/${encodeURIComponent(jobId)}/executions`, { params: { limit, offset } });
2792
+ return {
2793
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2794
+ };
2795
+ } catch (error) {
2796
+ const message = error instanceof Error ? error.message : String(error);
2797
+ return {
2798
+ content: [{ type: "text", text: `Failed to list cron job executions: ${message}` }],
2799
+ isError: true
2800
+ };
2801
+ }
2802
+ }
2803
+ );
2804
+ server.registerTool(
2805
+ "cron_jobs_list_all",
2806
+ {
2807
+ description: "List all cron jobs across projects (admin overview)",
2808
+ inputSchema: {
2809
+ projectId: z9.string().optional()
2810
+ }
2811
+ },
2812
+ async ({ projectId }) => {
2813
+ try {
2814
+ const result = await api.get("/cron/jobs", { params: { projectId } });
2815
+ return {
2816
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2817
+ };
2818
+ } catch (error) {
2819
+ const message = error instanceof Error ? error.message : String(error);
2820
+ return {
2821
+ content: [{ type: "text", text: `Failed to list all cron jobs: ${message}` }],
2822
+ isError: true
2823
+ };
2824
+ }
2825
+ }
2826
+ );
2827
+ server.registerTool(
2828
+ "cron_jobs_status",
2829
+ {
2830
+ description: "Get cron queue status",
2831
+ inputSchema: {}
2832
+ },
2833
+ async () => {
2834
+ try {
2835
+ const result = await api.get("/cron/status");
2836
+ return {
2837
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2838
+ };
2839
+ } catch (error) {
2840
+ const message = error instanceof Error ? error.message : String(error);
2841
+ return {
2842
+ content: [{ type: "text", text: `Failed to get cron status: ${message}` }],
2843
+ isError: true
2844
+ };
2845
+ }
2846
+ }
2847
+ );
2848
+ }
2849
+
2850
+ // libs/mcp-server/src/tools/webhooks.ts
2851
+ import { z as z10 } from "zod";
2852
+ var WEBHOOK_EVENT_TYPES = [
2853
+ "user.created",
2854
+ "user.updated",
2855
+ "user.deleted",
2856
+ "user.login",
2857
+ "user.email_verified",
2858
+ "user.password_reset_requested",
2859
+ "user.password_reset",
2860
+ "user.2fa_verified",
2861
+ "database.insert",
2862
+ "database.update",
2863
+ "database.delete",
2864
+ "storage.upload",
2865
+ "storage.delete",
2866
+ "project.created",
2867
+ "project.updated"
2868
+ ];
2869
+ function registerWebhookTools(server, api) {
2870
+ server.registerTool(
2871
+ "webhooks_list",
2872
+ {
2873
+ description: "List all webhooks for a project",
2874
+ inputSchema: {
2875
+ projectId: z10.string()
2876
+ }
2877
+ },
2878
+ async ({ projectId }) => {
2879
+ try {
2880
+ const result = await api.get(`/projects/${encodeURIComponent(projectId)}/webhooks`);
2881
+ return {
2882
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2883
+ };
2884
+ } catch (error) {
2885
+ const message = error instanceof Error ? error.message : String(error);
2886
+ return {
2887
+ content: [{ type: "text", text: `Failed to list webhooks: ${message}` }],
2888
+ isError: true
2889
+ };
2890
+ }
2891
+ }
2892
+ );
2893
+ server.registerTool(
2894
+ "webhooks_get",
2895
+ {
2896
+ description: "Get a single webhook by ID",
2897
+ inputSchema: {
2898
+ projectId: z10.string(),
2899
+ webhookId: z10.string()
2900
+ }
2901
+ },
2902
+ async ({ projectId, webhookId }) => {
2903
+ try {
2904
+ const result = await api.get(`/projects/${encodeURIComponent(projectId)}/webhooks/${encodeURIComponent(webhookId)}`);
2905
+ return {
2906
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2907
+ };
2908
+ } catch (error) {
2909
+ const message = error instanceof Error ? error.message : String(error);
2910
+ return {
2911
+ content: [{ type: "text", text: `Failed to get webhook: ${message}` }],
2912
+ isError: true
2913
+ };
2914
+ }
2915
+ }
2916
+ );
2917
+ server.registerTool(
2918
+ "webhooks_create",
2919
+ {
2920
+ 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.",
2921
+ inputSchema: {
2922
+ projectId: z10.string(),
2923
+ url: z10.string().url(),
2924
+ events: z10.array(z10.enum(WEBHOOK_EVENT_TYPES)).min(1),
2925
+ description: z10.string().optional(),
2926
+ headers: z10.record(z10.string(), z10.string()).optional(),
2927
+ active: z10.boolean().optional()
2928
+ }
2929
+ },
2930
+ async ({ projectId, url, events, description, headers, active }) => {
2931
+ try {
2932
+ const body = { url, events };
2933
+ if (description !== void 0) body.description = description;
2934
+ if (headers !== void 0) body.headers = headers;
2935
+ if (active !== void 0) body.active = active;
2936
+ const result = await api.post(`/projects/${encodeURIComponent(projectId)}/webhooks`, { body });
2937
+ return {
2938
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2939
+ };
2940
+ } catch (error) {
2941
+ const message = error instanceof Error ? error.message : String(error);
2942
+ return {
2943
+ content: [{ type: "text", text: `Failed to create webhook: ${message}` }],
2944
+ isError: true
2945
+ };
2946
+ }
2947
+ }
2948
+ );
2949
+ server.registerTool(
2950
+ "webhooks_update",
2951
+ {
2952
+ description: "Update an existing webhook",
2953
+ inputSchema: {
2954
+ projectId: z10.string(),
2955
+ webhookId: z10.string(),
2956
+ url: z10.string().url().optional(),
2957
+ events: z10.array(z10.enum(WEBHOOK_EVENT_TYPES)).min(1).optional(),
2958
+ description: z10.string().optional(),
2959
+ headers: z10.record(z10.string(), z10.string()).optional(),
2960
+ active: z10.boolean().optional()
2961
+ }
2962
+ },
2963
+ async ({ projectId, webhookId, url, events, description, headers, active }) => {
2964
+ try {
2965
+ const body = {};
2966
+ if (url !== void 0) body.url = url;
2967
+ if (events !== void 0) body.events = events;
2968
+ if (description !== void 0) body.description = description;
2969
+ if (headers !== void 0) body.headers = headers;
2970
+ if (active !== void 0) body.active = active;
2971
+ if (Object.keys(body).length === 0) {
2972
+ return {
2973
+ content: [{ type: "text", text: "At least one field must be provided" }],
2974
+ isError: true
2975
+ };
2976
+ }
2977
+ const result = await api.patch(`/projects/${encodeURIComponent(projectId)}/webhooks/${encodeURIComponent(webhookId)}`, { body });
2978
+ return {
2979
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2980
+ };
2981
+ } catch (error) {
2982
+ const message = error instanceof Error ? error.message : String(error);
2983
+ return {
2984
+ content: [{ type: "text", text: `Failed to update webhook: ${message}` }],
2985
+ isError: true
2986
+ };
2987
+ }
2988
+ }
2989
+ );
2990
+ server.registerTool(
2991
+ "webhooks_delete",
2992
+ {
2993
+ description: "Delete a webhook",
2994
+ inputSchema: {
2995
+ projectId: z10.string(),
2996
+ webhookId: z10.string()
2997
+ }
2998
+ },
2999
+ async ({ projectId, webhookId }) => {
3000
+ try {
3001
+ const result = await api.delete(`/projects/${encodeURIComponent(projectId)}/webhooks/${encodeURIComponent(webhookId)}`);
3002
+ return {
3003
+ content: [{ type: "text", text: result ? JSON.stringify(result, null, 2) : "Webhook deleted" }]
3004
+ };
3005
+ } catch (error) {
3006
+ const message = error instanceof Error ? error.message : String(error);
3007
+ return {
3008
+ content: [{ type: "text", text: `Failed to delete webhook: ${message}` }],
3009
+ isError: true
3010
+ };
3011
+ }
3012
+ }
3013
+ );
3014
+ server.registerTool(
3015
+ "webhooks_test",
3016
+ {
3017
+ description: "Send a test ping to a webhook",
3018
+ inputSchema: {
3019
+ projectId: z10.string(),
3020
+ webhookId: z10.string()
3021
+ }
3022
+ },
3023
+ async ({ projectId, webhookId }) => {
3024
+ try {
3025
+ const result = await api.post(`/projects/${encodeURIComponent(projectId)}/webhooks/${encodeURIComponent(webhookId)}/test`);
3026
+ return {
3027
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3028
+ };
3029
+ } catch (error) {
3030
+ const message = error instanceof Error ? error.message : String(error);
3031
+ return {
3032
+ content: [{ type: "text", text: `Failed to test webhook: ${message}` }],
3033
+ isError: true
3034
+ };
3035
+ }
3036
+ }
3037
+ );
3038
+ server.registerTool(
3039
+ "webhooks_deliveries",
3040
+ {
3041
+ description: "List webhook delivery logs for a project",
3042
+ inputSchema: {
3043
+ projectId: z10.string(),
3044
+ webhookId: z10.string().optional(),
3045
+ limit: z10.number().int().min(1).max(100).optional(),
3046
+ offset: z10.number().int().min(0).optional()
3047
+ }
3048
+ },
3049
+ async ({ projectId, webhookId, limit, offset }) => {
3050
+ try {
3051
+ const result = await api.get(`/projects/${encodeURIComponent(projectId)}/webhook-deliveries`, { params: { webhookId, limit, offset } });
3052
+ return {
3053
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3054
+ };
3055
+ } catch (error) {
3056
+ const message = error instanceof Error ? error.message : String(error);
3057
+ return {
3058
+ content: [{ type: "text", text: `Failed to list webhook deliveries: ${message}` }],
3059
+ isError: true
3060
+ };
3061
+ }
3062
+ }
3063
+ );
3064
+ server.registerTool(
3065
+ "webhooks_delivery_retry",
3066
+ {
3067
+ description: "Retry a failed webhook delivery",
3068
+ inputSchema: {
3069
+ projectId: z10.string(),
3070
+ deliveryId: z10.string()
3071
+ }
3072
+ },
3073
+ async ({ projectId, deliveryId }) => {
3074
+ try {
3075
+ const result = await api.post(`/projects/${encodeURIComponent(projectId)}/webhook-deliveries/${encodeURIComponent(deliveryId)}/retry`);
3076
+ return {
3077
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3078
+ };
3079
+ } catch (error) {
3080
+ const message = error instanceof Error ? error.message : String(error);
3081
+ return {
3082
+ content: [{ type: "text", text: `Failed to retry webhook delivery: ${message}` }],
3083
+ isError: true
3084
+ };
3085
+ }
3086
+ }
3087
+ );
3088
+ }
3089
+
3090
+ // libs/mcp-server/src/tools/notifications.ts
3091
+ import { z as z11 } from "zod";
3092
+ function registerNotificationTools(server, api) {
3093
+ server.registerTool(
3094
+ "notification_templates_list",
3095
+ {
3096
+ description: "List all notification templates for a project (includes system defaults)",
3097
+ inputSchema: {
3098
+ projectId: z11.string()
3099
+ }
3100
+ },
3101
+ async ({ projectId }) => {
3102
+ try {
3103
+ const result = await api.get(`/projects/${encodeURIComponent(projectId)}/notification-templates`);
3104
+ return {
3105
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3106
+ };
3107
+ } catch (error) {
3108
+ const message = error instanceof Error ? error.message : String(error);
3109
+ return {
3110
+ content: [{ type: "text", text: `Failed to list notification templates: ${message}` }],
3111
+ isError: true
3112
+ };
3113
+ }
3114
+ }
3115
+ );
3116
+ server.registerTool(
3117
+ "notification_templates_get",
3118
+ {
3119
+ description: "Get a single notification template by ID",
3120
+ inputSchema: {
3121
+ id: z11.string()
3122
+ }
3123
+ },
3124
+ async ({ id }) => {
3125
+ try {
3126
+ const result = await api.get(`/notification-templates/${encodeURIComponent(id)}`);
3127
+ return {
3128
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3129
+ };
3130
+ } catch (error) {
3131
+ const message = error instanceof Error ? error.message : String(error);
3132
+ return {
3133
+ content: [{ type: "text", text: `Failed to get notification template: ${message}` }],
3134
+ isError: true
3135
+ };
3136
+ }
3137
+ }
3138
+ );
3139
+ server.registerTool(
3140
+ "notification_templates_create",
3141
+ {
3142
+ description: "Create a new project-specific notification template. Variables use Handlebars syntax in titleTemplate and bodyTemplate.",
3143
+ inputSchema: {
3144
+ projectId: z11.string(),
3145
+ type: z11.string(),
3146
+ name: z11.string(),
3147
+ titleTemplate: z11.string(),
3148
+ bodyTemplate: z11.string(),
3149
+ icon: z11.string().optional(),
3150
+ defaultUrl: z11.string().optional(),
3151
+ variables: z11.array(z11.string()).optional(),
3152
+ isActive: z11.boolean().optional()
3153
+ }
3154
+ },
3155
+ async ({ projectId, type, name, titleTemplate, bodyTemplate, icon, defaultUrl, variables, isActive }) => {
3156
+ try {
3157
+ const body = { projectId, type, name, titleTemplate, bodyTemplate };
3158
+ if (icon !== void 0) body.icon = icon;
3159
+ if (defaultUrl !== void 0) body.defaultUrl = defaultUrl;
3160
+ if (variables !== void 0) body.variables = variables;
3161
+ if (isActive !== void 0) body.isActive = isActive;
3162
+ const result = await api.post(`/projects/${encodeURIComponent(projectId)}/notification-templates`, { body });
3163
+ return {
3164
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3165
+ };
3166
+ } catch (error) {
3167
+ const message = error instanceof Error ? error.message : String(error);
3168
+ return {
3169
+ content: [{ type: "text", text: `Failed to create notification template: ${message}` }],
3170
+ isError: true
3171
+ };
3172
+ }
3173
+ }
3174
+ );
3175
+ server.registerTool(
3176
+ "notification_templates_update",
3177
+ {
3178
+ description: "Update an existing notification template",
3179
+ inputSchema: {
3180
+ id: z11.string(),
3181
+ name: z11.string().optional(),
3182
+ titleTemplate: z11.string().optional(),
3183
+ bodyTemplate: z11.string().optional(),
3184
+ icon: z11.string().optional(),
3185
+ defaultUrl: z11.string().optional(),
3186
+ variables: z11.array(z11.string()).optional(),
3187
+ isActive: z11.boolean().optional()
3188
+ }
3189
+ },
3190
+ async ({ id, name, titleTemplate, bodyTemplate, icon, defaultUrl, variables, isActive }) => {
3191
+ try {
3192
+ const body = {};
3193
+ if (name !== void 0) body.name = name;
3194
+ if (titleTemplate !== void 0) body.titleTemplate = titleTemplate;
3195
+ if (bodyTemplate !== void 0) body.bodyTemplate = bodyTemplate;
3196
+ if (icon !== void 0) body.icon = icon;
3197
+ if (defaultUrl !== void 0) body.defaultUrl = defaultUrl;
3198
+ if (variables !== void 0) body.variables = variables;
3199
+ if (isActive !== void 0) body.isActive = isActive;
3200
+ if (Object.keys(body).length === 0) {
3201
+ return {
3202
+ content: [{ type: "text", text: "At least one field must be provided" }],
3203
+ isError: true
3204
+ };
3205
+ }
3206
+ const result = await api.patch(`/notification-templates/${encodeURIComponent(id)}`, { body });
3207
+ return {
3208
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3209
+ };
3210
+ } catch (error) {
3211
+ const message = error instanceof Error ? error.message : String(error);
3212
+ return {
3213
+ content: [{ type: "text", text: `Failed to update notification template: ${message}` }],
3214
+ isError: true
3215
+ };
3216
+ }
3217
+ }
3218
+ );
3219
+ server.registerTool(
3220
+ "notification_templates_delete",
3221
+ {
3222
+ description: "Delete a project notification template override (reverts to system default)",
3223
+ inputSchema: {
3224
+ id: z11.string()
3225
+ }
3226
+ },
3227
+ async ({ id }) => {
3228
+ try {
3229
+ const result = await api.delete(`/notification-templates/${encodeURIComponent(id)}`);
3230
+ return {
3231
+ content: [{ type: "text", text: result ? JSON.stringify(result, null, 2) : "Notification template deleted" }]
3232
+ };
3233
+ } catch (error) {
3234
+ const message = error instanceof Error ? error.message : String(error);
3235
+ return {
3236
+ content: [{ type: "text", text: `Failed to delete notification template: ${message}` }],
3237
+ isError: true
3238
+ };
3239
+ }
3240
+ }
3241
+ );
3242
+ server.registerTool(
3243
+ "notification_templates_preview",
3244
+ {
3245
+ description: "Preview a compiled notification template with Handlebars variables applied",
3246
+ inputSchema: {
3247
+ titleTemplate: z11.string(),
3248
+ bodyTemplate: z11.string(),
3249
+ variables: z11.record(z11.string(), z11.string())
3250
+ }
3251
+ },
3252
+ async ({ titleTemplate, bodyTemplate, variables }) => {
3253
+ try {
3254
+ const result = await api.post("/notification-templates/preview", { body: { titleTemplate, bodyTemplate, variables } });
3255
+ return {
3256
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3257
+ };
3258
+ } catch (error) {
3259
+ const message = error instanceof Error ? error.message : String(error);
3260
+ return {
3261
+ content: [{ type: "text", text: `Failed to preview notification template: ${message}` }],
3262
+ isError: true
3263
+ };
3264
+ }
3265
+ }
3266
+ );
3267
+ server.registerTool(
3268
+ "notification_config_get",
3269
+ {
3270
+ 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.",
3271
+ inputSchema: {
3272
+ projectId: z11.string()
3273
+ }
3274
+ },
3275
+ async ({ projectId }) => {
3276
+ try {
3277
+ const result = await api.get(`/projects/${encodeURIComponent(projectId)}/notification-config`);
3278
+ return {
3279
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3280
+ };
3281
+ } catch (error) {
3282
+ const message = error instanceof Error ? error.message : String(error);
3283
+ return {
3284
+ content: [{ type: "text", text: `Failed to get notification config: ${message}` }],
3285
+ isError: true
3286
+ };
3287
+ }
3288
+ }
3289
+ );
3290
+ server.registerTool(
3291
+ "notification_config_update",
3292
+ {
3293
+ 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.",
3294
+ inputSchema: {
3295
+ projectId: z11.string(),
3296
+ vapid: z11.object({
3297
+ publicKey: z11.string().optional(),
3298
+ privateKey: z11.string().optional(),
3299
+ subject: z11.string().optional(),
3300
+ enabled: z11.boolean().optional()
3301
+ }).optional(),
3302
+ fcm: z11.object({
3303
+ serviceAccountJson: z11.string().optional(),
3304
+ enabled: z11.boolean().optional()
3305
+ }).optional(),
3306
+ apns: z11.object({
3307
+ keyId: z11.string().optional(),
3308
+ teamId: z11.string().optional(),
3309
+ privateKey: z11.string().optional(),
3310
+ bundleId: z11.string().optional(),
3311
+ production: z11.boolean().optional(),
3312
+ enabled: z11.boolean().optional()
3313
+ }).optional()
3314
+ }
3315
+ },
3316
+ async ({ projectId, vapid, fcm, apns }) => {
3317
+ try {
3318
+ const body = {};
3319
+ if (vapid !== void 0) body.vapid = vapid;
3320
+ if (fcm !== void 0) body.fcm = fcm;
3321
+ if (apns !== void 0) body.apns = apns;
3322
+ if (Object.keys(body).length === 0) {
3323
+ return {
3324
+ content: [{ type: "text", text: "At least one provider config must be provided" }],
3325
+ isError: true
3326
+ };
3327
+ }
3328
+ const result = await api.patch(`/projects/${encodeURIComponent(projectId)}/notification-config`, { body });
3329
+ return {
3330
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3331
+ };
3332
+ } catch (error) {
3333
+ const message = error instanceof Error ? error.message : String(error);
3334
+ return {
3335
+ content: [{ type: "text", text: `Failed to update notification config: ${message}` }],
3336
+ isError: true
3337
+ };
3338
+ }
3339
+ }
3340
+ );
3341
+ server.registerTool(
3342
+ "notification_config_delete",
3343
+ {
3344
+ description: "Delete push notification provider configuration. Specify a provider to delete only that one, or omit to delete all.",
3345
+ inputSchema: {
3346
+ projectId: z11.string(),
3347
+ provider: z11.enum(["vapid", "fcm", "apns"]).optional()
3348
+ }
3349
+ },
3350
+ async ({ projectId, provider }) => {
3351
+ try {
3352
+ const result = await api.delete(`/projects/${encodeURIComponent(projectId)}/notification-config`, { params: { provider } });
3353
+ return {
3354
+ content: [{ type: "text", text: result ? JSON.stringify(result, null, 2) : "Notification config deleted" }]
3355
+ };
3356
+ } catch (error) {
3357
+ const message = error instanceof Error ? error.message : String(error);
3358
+ return {
3359
+ content: [{ type: "text", text: `Failed to delete notification config: ${message}` }],
3360
+ isError: true
3361
+ };
3362
+ }
3363
+ }
3364
+ );
3365
+ server.registerTool(
3366
+ "notification_config_generate_vapid",
3367
+ {
3368
+ description: "Generate a new VAPID key pair for web push notifications. WARNING: The private key will be visible in the conversation context.",
3369
+ inputSchema: {}
3370
+ },
3371
+ async () => {
3372
+ try {
3373
+ const result = await api.post("/notification-config/generate-vapid-keys");
3374
+ return {
3375
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3376
+ };
3377
+ } catch (error) {
3378
+ const message = error instanceof Error ? error.message : String(error);
3379
+ return {
3380
+ content: [{ type: "text", text: `Failed to generate VAPID keys: ${message}` }],
3381
+ isError: true
3382
+ };
3383
+ }
3384
+ }
3385
+ );
3386
+ server.registerTool(
3387
+ "notifications_send",
3388
+ {
3389
+ description: "Send a push notification to selected users",
3390
+ inputSchema: {
3391
+ projectId: z11.string(),
3392
+ userIds: z11.array(z11.string()).min(1),
3393
+ title: z11.string(),
3394
+ body: z11.string(),
3395
+ icon: z11.string().optional(),
3396
+ url: z11.string().optional()
3397
+ }
3398
+ },
3399
+ async ({ projectId, userIds, title, body: notifBody, icon, url }) => {
3400
+ try {
3401
+ const reqBody = { userIds, title, body: notifBody };
3402
+ if (icon !== void 0) reqBody.icon = icon;
3403
+ if (url !== void 0) reqBody.url = url;
3404
+ const result = await api.post(`/projects/${encodeURIComponent(projectId)}/notifications/send`, { body: reqBody });
3405
+ return {
3406
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3407
+ };
3408
+ } catch (error) {
3409
+ const message = error instanceof Error ? error.message : String(error);
3410
+ return {
3411
+ content: [{ type: "text", text: `Failed to send notification: ${message}` }],
3412
+ isError: true
3413
+ };
3414
+ }
3415
+ }
3416
+ );
3417
+ server.registerTool(
3418
+ "notifications_broadcast",
3419
+ {
3420
+ description: "Broadcast a push notification to all subscribed users in a project",
3421
+ inputSchema: {
3422
+ projectId: z11.string(),
3423
+ title: z11.string(),
3424
+ body: z11.string(),
3425
+ icon: z11.string().optional(),
3426
+ url: z11.string().optional()
3427
+ }
3428
+ },
3429
+ async ({ projectId, title, body: notifBody, icon, url }) => {
3430
+ try {
3431
+ const reqBody = { title, body: notifBody };
3432
+ if (icon !== void 0) reqBody.icon = icon;
3433
+ if (url !== void 0) reqBody.url = url;
3434
+ const result = await api.post(`/projects/${encodeURIComponent(projectId)}/notifications/broadcast`, { body: reqBody });
3435
+ return {
3436
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3437
+ };
3438
+ } catch (error) {
3439
+ const message = error instanceof Error ? error.message : String(error);
3440
+ return {
3441
+ content: [{ type: "text", text: `Failed to broadcast notification: ${message}` }],
3442
+ isError: true
3443
+ };
3444
+ }
3445
+ }
3446
+ );
3447
+ }
3448
+
3449
+ // libs/mcp-server/src/tools/auditLogs.ts
3450
+ import { z as z12 } from "zod";
3451
+ function registerAuditLogTools(server, api) {
3452
+ server.registerTool(
3453
+ "audit_logs_list",
3454
+ {
3455
+ description: "List audit logs for a project. Filter by entity type, action, and date range.",
3456
+ inputSchema: {
3457
+ projectId: z12.string(),
3458
+ entityType: z12.enum(["user", "client", "project", "auth", "database", "storage"]).optional(),
3459
+ action: z12.string().optional(),
3460
+ startDate: z12.string().datetime().optional(),
3461
+ endDate: z12.string().datetime().optional(),
3462
+ limit: z12.number().int().min(1).max(100).optional(),
3463
+ offset: z12.number().int().min(0).optional()
3464
+ }
3465
+ },
3466
+ async ({ projectId, entityType, action, startDate, endDate, limit, offset }) => {
3467
+ try {
3468
+ const result = await api.get(`/projects/${encodeURIComponent(projectId)}/audit-logs`, {
3469
+ params: { entityType, action, startDate, endDate, limit, offset }
3470
+ });
3471
+ return {
3472
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3473
+ };
3474
+ } catch (error) {
3475
+ const message = error instanceof Error ? error.message : String(error);
3476
+ return {
3477
+ content: [{ type: "text", text: `Failed to list audit logs: ${message}` }],
3478
+ isError: true
3479
+ };
3480
+ }
3481
+ }
3482
+ );
3483
+ server.registerTool(
3484
+ "audit_logs_get",
3485
+ {
3486
+ description: "Get a single audit log entry by ID",
3487
+ inputSchema: {
3488
+ projectId: z12.string(),
3489
+ logId: z12.string()
3490
+ }
3491
+ },
3492
+ async ({ projectId, logId }) => {
3493
+ try {
3494
+ const result = await api.get(`/projects/${encodeURIComponent(projectId)}/audit-logs/${encodeURIComponent(logId)}`);
3495
+ return {
3496
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3497
+ };
3498
+ } catch (error) {
3499
+ const message = error instanceof Error ? error.message : String(error);
3500
+ return {
3501
+ content: [{ type: "text", text: `Failed to get audit log: ${message}` }],
3502
+ isError: true
3503
+ };
3504
+ }
3505
+ }
3506
+ );
3507
+ }
3508
+
3509
+ // libs/mcp-server/src/tools/index.ts
3510
+ function registerAllTools(server, api) {
3511
+ registerAuthTools(server, api);
3512
+ registerProjectTools(server, api);
3513
+ registerClientTools(server, api);
3514
+ registerDatabaseTools(server, api);
3515
+ registerHostingTools(server, api);
3516
+ registerStorageTools(server, api);
3517
+ registerFunctionTools(server, api);
3518
+ registerEmailTemplateTools(server, api);
3519
+ registerCronJobTools(server, api);
3520
+ registerWebhookTools(server, api);
3521
+ registerNotificationTools(server, api);
3522
+ registerAuditLogTools(server, api);
3523
+ }
3524
+
3525
+ // libs/mcp-server/src/prompts/database.ts
3526
+ import { z as z13 } from "zod";
3527
+ function registerDatabasePrompts(server) {
3528
+ server.registerPrompt(
3529
+ "setup-collection",
3530
+ {
3531
+ title: "Setup Database Collection",
3532
+ description: "Guide for creating a new database collection with proper security rules. Collections MUST have rules defined before data can be inserted.",
3533
+ argsSchema: {
3534
+ projectId: z13.string().describe("Project ID"),
3535
+ collectionName: z13.string().describe("Name of the collection to create"),
3536
+ access: z13.enum(["public-read", "authenticated", "admin-only"]).optional().describe("Access level (default: admin-only)")
3537
+ }
3538
+ },
3539
+ async ({ projectId, collectionName, access }) => {
3540
+ const accessLevel = access || "admin-only";
3541
+ const rules = {
3542
+ "public-read": {
3543
+ ".create": "true",
3544
+ ".read": "true",
3545
+ ".update": "false",
3546
+ ".delete": "auth.role === 'owner' || auth.role === 'admin'"
3547
+ },
3548
+ authenticated: {
3549
+ ".create": "!!auth.uid",
3550
+ ".read": "!!auth.uid",
3551
+ ".update": "!!auth.uid",
3552
+ ".delete": "auth.role === 'owner' || auth.role === 'admin'"
3553
+ },
3554
+ "admin-only": {
3555
+ ".create": "auth.role === 'owner' || auth.role === 'admin'",
3556
+ ".read": "auth.role === 'owner' || auth.role === 'admin'",
3557
+ ".update": "auth.role === 'owner' || auth.role === 'admin'",
3558
+ ".delete": "auth.role === 'owner' || auth.role === 'admin'"
3559
+ }
3560
+ };
3561
+ const collectionRules = rules[accessLevel];
3562
+ const rulesJson = JSON.stringify(collectionRules, null, 2);
3563
+ return {
3564
+ messages: [
3565
+ {
3566
+ role: "user",
3567
+ content: {
3568
+ type: "text",
3569
+ text: [
3570
+ `Set up a new database collection "${collectionName}" in project ${projectId}.`,
3571
+ "",
3572
+ "IMPORTANT WORKFLOW:",
3573
+ "1. First, get the current rules with database_rules_get",
3574
+ "2. Add the new collection rules to the existing rules object",
3575
+ "3. Set the updated rules with database_rules_set \u2014 this automatically creates the collection",
3576
+ '4. Do NOT try to insert documents before the rules are set \u2014 it will fail with "Rule denied"',
3577
+ "",
3578
+ `Suggested rules for "${collectionName}" (${accessLevel}):`,
3579
+ rulesJson,
3580
+ "",
3581
+ "The $other catch-all rule should deny access to undefined collections:",
3582
+ ' "$other": { ".read": "false", ".write": "false" }',
3583
+ "",
3584
+ "After rules are set, the collection is ready for use via:",
3585
+ "- database_documents_insert",
3586
+ "- database_documents_find",
3587
+ '- Serverless functions using spacelr.db.collection("' + collectionName + '")'
3588
+ ].join("\n")
3589
+ }
3590
+ }
3591
+ ]
3592
+ };
3593
+ }
3594
+ );
3595
+ server.registerPrompt(
3596
+ "database-workflow",
3597
+ {
3598
+ title: "Database Workflow Guide",
3599
+ description: "Explains how the Spacelr database system works: rules-first approach, collection lifecycle, and security model.",
3600
+ argsSchema: {
3601
+ projectId: z13.string().describe("Project ID")
3602
+ }
3603
+ },
3604
+ async ({ projectId }) => {
3605
+ return {
3606
+ messages: [
3607
+ {
3608
+ role: "user",
3609
+ content: {
3610
+ type: "text",
3611
+ text: [
3612
+ `Guide me through the Spacelr database system for project ${projectId}.`,
3613
+ "",
3614
+ "CORE CONCEPTS:",
3615
+ "",
3616
+ "1. RULES-FIRST: Collections are defined through security rules.",
3617
+ " - Use database_rules_get to see current rules",
3618
+ " - Use database_rules_set to add/modify collection rules",
3619
+ " - Setting rules automatically creates the collection definition",
3620
+ " - Without rules, all operations are denied by the $other catch-all",
3621
+ "",
3622
+ "2. SECURITY RULES SYNTAX:",
3623
+ ' - ".create": Expression that must evaluate to true for inserts',
3624
+ ' - ".read": Expression for reads/queries',
3625
+ ' - ".update": Expression for document updates',
3626
+ ' - ".delete": Expression for document deletion',
3627
+ ' - ".validate": Per-field validation expressions',
3628
+ ' - ".schema": Field type declarations',
3629
+ " Available context: auth.uid, auth.role, newData (for validation)",
3630
+ "",
3631
+ "3. WORKFLOW:",
3632
+ " a) Define rules for the collection",
3633
+ " b) Optionally create indexes (database_indexes_create)",
3634
+ " c) Insert/query documents",
3635
+ "",
3636
+ "4. DATA ISOLATION:",
3637
+ " - Each project has its own database (project_{projectId})",
3638
+ " - Collections are scoped to the project automatically",
3639
+ " - No cross-project data access is possible"
3640
+ ].join("\n")
3641
+ }
3642
+ }
3643
+ ]
3644
+ };
3645
+ }
3646
+ );
3647
+ }
3648
+
3649
+ // libs/mcp-server/src/prompts/functions.ts
3650
+ import { z as z14 } from "zod";
3651
+ function registerFunctionPrompts(server) {
3652
+ server.registerPrompt(
3653
+ "deploy-function",
3654
+ {
3655
+ title: "Deploy a Serverless Function",
3656
+ description: "Step-by-step guide for creating, deploying, and testing a serverless function. Covers the full lifecycle from creation to execution.",
3657
+ argsSchema: {
3658
+ projectId: z14.string().describe("Project ID"),
3659
+ name: z14.string().describe("Function name"),
3660
+ useCase: z14.string().optional().describe('What the function should do (e.g. "fetch data from API and store in DB")')
3661
+ }
3662
+ },
3663
+ async ({ projectId, name, useCase }) => {
3664
+ return {
3665
+ messages: [
3666
+ {
3667
+ role: "user",
3668
+ content: {
3669
+ type: "text",
3670
+ text: [
3671
+ `Deploy a serverless function "${name}" in project ${projectId}.`,
3672
+ useCase ? `Use case: ${useCase}` : "",
3673
+ "",
3674
+ "DEPLOYMENT WORKFLOW:",
3675
+ "",
3676
+ "1. CREATE the function:",
3677
+ " Use functions_create with name, entryPoint (default: index.js),",
3678
+ " timeout (ms, default: 30000), memoryLimitMb (default: 128)",
3679
+ " Optional: cronExpression for scheduled execution",
3680
+ "",
3681
+ "2. WRITE the code:",
3682
+ " Create an index.js file. Available APIs inside the sandbox:",
3683
+ "",
3684
+ " - console.log/warn/error/info() \u2014 captured to execution logs",
3685
+ " - await fetch(url, options) \u2014 HTTP client (10s timeout, 5MB limit)",
3686
+ " Response: { status, headers, body } where body is a STRING",
3687
+ " Use JSON.parse(response.body) for JSON APIs",
3688
+ " - env.get(key) \u2014 read environment variables (API keys, secrets \u2014 set via Console UI or CLI)",
3689
+ " - await kv.get/set/delete/list() \u2014 Redis KV store (256KB values, 1000 keys)",
3690
+ " - await spacelr.db.collection(name).find/insertOne/insertMany()",
3691
+ " - await spacelr.storage.list/getInfo/getDownloadUrl()",
3692
+ " - await spacelr.email.send/sendRaw() \u2014 send template or raw emails",
3693
+ " - await spacelr.notifications.send/sendMany() \u2014 push notifications",
3694
+ "",
3695
+ " IMPORTANT: Top-level await is supported. No imports/require available.",
3696
+ "",
3697
+ "3. IF USING spacelr.db:",
3698
+ " Database rules MUST be set BEFORE the function writes to a collection.",
3699
+ " Use the setup-collection prompt or manually set rules via database_rules_set.",
3700
+ ' Without rules, inserts will fail with "Rule denied".',
3701
+ "",
3702
+ "4. DEPLOY the code:",
3703
+ " Zip the file(s) and upload via the CLI:",
3704
+ " spacelr functions deploy ./my-function --name " + name,
3705
+ " Or use the admin API: POST /projects/{projectId}/functions/{id}/deploy",
3706
+ "",
3707
+ "5. TRIGGER:",
3708
+ " Functions support multiple trigger types:",
3709
+ "",
3710
+ " a) Manual: Use functions_trigger to execute on demand.",
3711
+ " b) Cron: Set cronExpression when creating/updating the function.",
3712
+ " c) Webhook: Enable with functions_enable_webhook to get a secret,",
3713
+ " then POST to /api/v1/functions/{projectId}/{functionId}/invoke",
3714
+ " with the X-Webhook-Secret header. Use functions_regenerate_webhook_secret",
3715
+ " to rotate the secret (old secret stops working immediately).",
3716
+ " d) Event: Configure event triggers to run the function in response",
2514
3717
  " to platform events (e.g. database changes, storage uploads).",
2515
3718
  "",
2516
3719
  " Check results with functions_executions_list.",
@@ -2557,9 +3760,12 @@ function registerFunctionPrompts(server) {
2557
3760
  " NOTE: body is always a string \u2014 use JSON.parse(response.body) for JSON",
2558
3761
  " Limits: 10s timeout, 5MB response, no private/internal URLs (SSRF blocked)",
2559
3762
  "",
2560
- "\u2550\u2550\u2550 ENV \u2550\u2550\u2550",
3763
+ "\u2550\u2550\u2550 ENV (Environment Variables) \u2550\u2550\u2550",
2561
3764
  "env.get(key) \u2192 string | undefined",
2562
3765
  "Read-only access to function environment variables.",
3766
+ "Use env vars to store API keys, secrets, and configuration \u2014 never hardcode them in function code.",
3767
+ "Set via Console UI (Function Detail \u2192 Environment Variables) or CLI (spacelr functions env set).",
3768
+ "Values are encrypted at rest and only decrypted when the function executes.",
2563
3769
  "",
2564
3770
  "\u2550\u2550\u2550 KV STORE \u2550\u2550\u2550",
2565
3771
  "await kv.get(key) \u2192 string | null",
@@ -2582,6 +3788,24 @@ function registerFunctionPrompts(server) {
2582
3788
  "await spacelr.storage.getDownloadUrl(fileId) \u2192 string",
2583
3789
  "Scoped to the function's project.",
2584
3790
  "",
3791
+ "\u2550\u2550\u2550 EMAIL \u2550\u2550\u2550",
3792
+ "await spacelr.email.send({ to, template, variables })",
3793
+ " Sends a template-based email using project templates (MJML + Handlebars).",
3794
+ " template: template name (must exist in the project)",
3795
+ " variables: Record<string, string> of template variables",
3796
+ " Returns: { messageId }",
3797
+ "await spacelr.email.sendRaw({ to, subject, html })",
3798
+ " Sends a raw HTML email directly. Max 500KB HTML.",
3799
+ " Returns: { messageId }",
3800
+ "Rate limit: 100 emails/hour per project (configurable via FUNCTION_EMAIL_HOURLY_LIMIT).",
3801
+ "",
3802
+ "\u2550\u2550\u2550 NOTIFICATIONS \u2550\u2550\u2550",
3803
+ "await spacelr.notifications.send({ userId, title, body, data? })",
3804
+ " Sends a push notification to a specific user in the project.",
3805
+ "await spacelr.notifications.sendMany({ userIds, title, body, data? })",
3806
+ " Sends to multiple users. Deduplicates userIds automatically.",
3807
+ "Rate limit: 500 notifications/hour per project (configurable via FUNCTION_NOTIFICATION_HOURLY_LIMIT). Scoped to the function's project.",
3808
+ "",
2585
3809
  "\u2550\u2550\u2550 RESOURCE LIMITS \u2550\u2550\u2550",
2586
3810
  "Execution timeout: 30s default, 120s max",
2587
3811
  "Memory: 128MB default, 512MB max",