@spacelr/mcp 0.0.4 → 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);
@@ -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,6 +2383,178 @@ function registerFunctionTools(server, api) {
2310
2383
  );
2311
2384
  }
2312
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
+
2313
2558
  // libs/mcp-server/src/tools/index.ts
2314
2559
  function registerAllTools(server, api) {
2315
2560
  registerAuthTools(server, api);
@@ -2319,10 +2564,11 @@ function registerAllTools(server, api) {
2319
2564
  registerHostingTools(server, api);
2320
2565
  registerStorageTools(server, api);
2321
2566
  registerFunctionTools(server, api);
2567
+ registerEmailTemplateTools(server, api);
2322
2568
  }
2323
2569
 
2324
2570
  // libs/mcp-server/src/prompts/database.ts
2325
- import { z as z8 } from "zod";
2571
+ import { z as z9 } from "zod";
2326
2572
  function registerDatabasePrompts(server) {
2327
2573
  server.registerPrompt(
2328
2574
  "setup-collection",
@@ -2330,9 +2576,9 @@ function registerDatabasePrompts(server) {
2330
2576
  title: "Setup Database Collection",
2331
2577
  description: "Guide for creating a new database collection with proper security rules. Collections MUST have rules defined before data can be inserted.",
2332
2578
  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)")
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)")
2336
2582
  }
2337
2583
  },
2338
2584
  async ({ projectId, collectionName, access }) => {
@@ -2397,7 +2643,7 @@ function registerDatabasePrompts(server) {
2397
2643
  title: "Database Workflow Guide",
2398
2644
  description: "Explains how the Spacelr database system works: rules-first approach, collection lifecycle, and security model.",
2399
2645
  argsSchema: {
2400
- projectId: z8.string().describe("Project ID")
2646
+ projectId: z9.string().describe("Project ID")
2401
2647
  }
2402
2648
  },
2403
2649
  async ({ projectId }) => {
@@ -2446,7 +2692,7 @@ function registerDatabasePrompts(server) {
2446
2692
  }
2447
2693
 
2448
2694
  // libs/mcp-server/src/prompts/functions.ts
2449
- import { z as z9 } from "zod";
2695
+ import { z as z10 } from "zod";
2450
2696
  function registerFunctionPrompts(server) {
2451
2697
  server.registerPrompt(
2452
2698
  "deploy-function",
@@ -2454,9 +2700,9 @@ function registerFunctionPrompts(server) {
2454
2700
  title: "Deploy a Serverless Function",
2455
2701
  description: "Step-by-step guide for creating, deploying, and testing a serverless function. Covers the full lifecycle from creation to execution.",
2456
2702
  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")')
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")')
2460
2706
  }
2461
2707
  },
2462
2708
  async ({ projectId, name, useCase }) => {
@@ -2484,10 +2730,12 @@ function registerFunctionPrompts(server) {
2484
2730
  " - await fetch(url, options) \u2014 HTTP client (10s timeout, 5MB limit)",
2485
2731
  " Response: { status, headers, body } where body is a STRING",
2486
2732
  " Use JSON.parse(response.body) for JSON APIs",
2487
- " - env.get(key) \u2014 read environment variables",
2733
+ " - env.get(key) \u2014 read environment variables (API keys, secrets \u2014 set via Console UI or CLI)",
2488
2734
  " - await kv.get/set/delete/list() \u2014 Redis KV store (256KB values, 1000 keys)",
2489
2735
  " - await spacelr.db.collection(name).find/insertOne/insertMany()",
2490
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",
2491
2739
  "",
2492
2740
  " IMPORTANT: Top-level await is supported. No imports/require available.",
2493
2741
  "",
@@ -2557,9 +2805,12 @@ function registerFunctionPrompts(server) {
2557
2805
  " NOTE: body is always a string \u2014 use JSON.parse(response.body) for JSON",
2558
2806
  " Limits: 10s timeout, 5MB response, no private/internal URLs (SSRF blocked)",
2559
2807
  "",
2560
- "\u2550\u2550\u2550 ENV \u2550\u2550\u2550",
2808
+ "\u2550\u2550\u2550 ENV (Environment Variables) \u2550\u2550\u2550",
2561
2809
  "env.get(key) \u2192 string | undefined",
2562
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.",
2563
2814
  "",
2564
2815
  "\u2550\u2550\u2550 KV STORE \u2550\u2550\u2550",
2565
2816
  "await kv.get(key) \u2192 string | null",
@@ -2582,6 +2833,24 @@ function registerFunctionPrompts(server) {
2582
2833
  "await spacelr.storage.getDownloadUrl(fileId) \u2192 string",
2583
2834
  "Scoped to the function's project.",
2584
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
+ "",
2585
2854
  "\u2550\u2550\u2550 RESOURCE LIMITS \u2550\u2550\u2550",
2586
2855
  "Execution timeout: 30s default, 120s max",
2587
2856
  "Memory: 128MB default, 512MB max",