@spacelr/mcp 0.0.11 → 0.0.13

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
@@ -597,6 +597,53 @@ function registerAuthTools(server, api) {
597
597
  }
598
598
  }
599
599
  );
600
+ server.registerTool(
601
+ "auth_verify_two_factor",
602
+ {
603
+ description: "Complete a two-factor authentication login. Call this after auth_login returns twoFactorRequired: true. Note: device trust is not persisted in MCP sessions \u2014 2FA will be required on every login. WARNING: token and code are visible in the conversation.",
604
+ inputSchema: {
605
+ token: z.string().min(1),
606
+ code: z.string().trim().regex(/^\d{6}$/, "Must be a 6-digit numeric code")
607
+ }
608
+ },
609
+ async ({ token, code }) => {
610
+ try {
611
+ const result = await api.post("/auth/verify-two-factor", { body: { token, code } });
612
+ return {
613
+ content: [{ type: "text", text: JSON.stringify(redactTokens(result), null, 2) }]
614
+ };
615
+ } catch (error) {
616
+ const message = error instanceof Error ? error.message : String(error);
617
+ return {
618
+ content: [{ type: "text", text: `Two-factor verification failed: ${message}` }],
619
+ isError: true
620
+ };
621
+ }
622
+ }
623
+ );
624
+ server.registerTool(
625
+ "auth_resend_two_factor_code",
626
+ {
627
+ description: "Resend the two-factor authentication code. Rate-limited to 3 requests per minute. Requires the twoFactorToken from auth_login. WARNING: the token is sensitive and will be visible in the conversation.",
628
+ inputSchema: {
629
+ token: z.string().min(1)
630
+ }
631
+ },
632
+ async ({ token }) => {
633
+ try {
634
+ const result = await api.post("/auth/resend-two-factor-code", { body: { token } });
635
+ return {
636
+ content: [{ type: "text", text: JSON.stringify(redactTokens(result), null, 2) }]
637
+ };
638
+ } catch (error) {
639
+ const message = error instanceof Error ? error.message : String(error);
640
+ return {
641
+ content: [{ type: "text", text: `Failed to resend two-factor code: ${message}` }],
642
+ isError: true
643
+ };
644
+ }
645
+ }
646
+ );
600
647
  }
601
648
 
602
649
  // libs/mcp-server/src/tools/projects.ts
@@ -1178,6 +1225,50 @@ function registerDatabaseTools(server, api) {
1178
1225
  }
1179
1226
  }
1180
1227
  );
1228
+ server.registerTool(
1229
+ "database_collections_update_metadata",
1230
+ {
1231
+ description: "Update a collection's realtime delivery mode and optional stream retention. `realtimeMode: 'stream'` switches the collection to Redis Streams for durable event replay (needed for chat, notifications, activity feeds). `realtimeMode: 'pubsub'` (default) is fire-and-forget, used for dashboards and list views. Exactly one of `streamRetention.maxLen` (approx. entry count) or `streamRetention.maxAgeMs` (retention age in ms) may be set. Response may include `warnings[]` if the project exceeds the 50-collection soft cap.",
1232
+ inputSchema: {
1233
+ projectId: z4.string(),
1234
+ name: z4.string(),
1235
+ realtimeMode: z4.enum(["pubsub", "stream"]).optional(),
1236
+ streamRetention: z4.object({
1237
+ maxLen: z4.number().int().min(1).optional(),
1238
+ maxAgeMs: z4.number().int().min(1e3).optional()
1239
+ }).refine(
1240
+ (r) => !(r.maxLen !== void 0 && r.maxAgeMs !== void 0),
1241
+ { message: "Set at most one of maxLen or maxAgeMs" }
1242
+ ).optional()
1243
+ }
1244
+ },
1245
+ async ({ projectId, name, realtimeMode, streamRetention }) => {
1246
+ try {
1247
+ const body = {};
1248
+ if (realtimeMode !== void 0) body.realtimeMode = realtimeMode;
1249
+ if (streamRetention !== void 0) body.streamRetention = streamRetention;
1250
+ if (Object.keys(body).length === 0) {
1251
+ return {
1252
+ content: [{ type: "text", text: "At least one of realtimeMode or streamRetention must be provided" }],
1253
+ isError: true
1254
+ };
1255
+ }
1256
+ const result = await api.patch(
1257
+ `/databases/${encodeURIComponent(projectId)}/collections/${encodeURIComponent(name)}`,
1258
+ { body }
1259
+ );
1260
+ return {
1261
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1262
+ };
1263
+ } catch (error) {
1264
+ const message = error instanceof Error ? error.message : String(error);
1265
+ return {
1266
+ content: [{ type: "text", text: `Failed to update collection metadata: ${message}` }],
1267
+ isError: true
1268
+ };
1269
+ }
1270
+ }
1271
+ );
1181
1272
  server.registerTool(
1182
1273
  "database_collections_delete",
1183
1274
  {
@@ -1592,6 +1683,9 @@ function registerDatabaseTools(server, api) {
1592
1683
  }
1593
1684
 
1594
1685
  // libs/mcp-server/src/tools/hosting.ts
1686
+ import { readdir, readFile } from "fs/promises";
1687
+ import { lstatSync } from "fs";
1688
+ import { join as join2, relative, resolve, sep } from "path";
1595
1689
  import { z as z5 } from "zod";
1596
1690
 
1597
1691
  // libs/mcp-server/src/zip.ts
@@ -1607,17 +1701,29 @@ var ZipBuilder = class {
1607
1701
  * @param content - UTF-8 text content of the file
1608
1702
  */
1609
1703
  addFile(name, content) {
1704
+ this.addFileBuffer(name, Buffer.from(content, "utf-8"));
1705
+ }
1706
+ /**
1707
+ * Add a binary file to the archive.
1708
+ *
1709
+ * @param name - File path inside the ZIP (e.g. "images/logo.png")
1710
+ * @param content - Raw file content as a Buffer
1711
+ */
1712
+ addFileBuffer(name, content) {
1610
1713
  const nameBuffer = Buffer.from(name, "utf-8");
1611
- const contentBuffer = Buffer.from(content, "utf-8");
1612
- const compressed = deflateRawSync(contentBuffer, { level: 9 });
1613
- const crc32 = computeCrc32(contentBuffer);
1714
+ const compressed = deflateRawSync(content, { level: 9 });
1715
+ const crc32 = computeCrc32(content);
1614
1716
  this.files.push({
1615
1717
  name: nameBuffer,
1616
- content: contentBuffer,
1718
+ content,
1617
1719
  compressed,
1618
1720
  crc32
1619
1721
  });
1620
1722
  }
1723
+ /** Number of files added to the archive. */
1724
+ get fileCount() {
1725
+ return this.files.length;
1726
+ }
1621
1727
  /** Generate the complete ZIP archive as a Buffer. */
1622
1728
  toBuffer() {
1623
1729
  const localHeaders = [];
@@ -1691,6 +1797,17 @@ function computeCrc32(data) {
1691
1797
  }
1692
1798
 
1693
1799
  // libs/mcp-server/src/tools/hosting.ts
1800
+ async function walkDir(dir, base, zip) {
1801
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
1802
+ const fullPath = join2(dir, entry.name);
1803
+ if (entry.isSymbolicLink()) continue;
1804
+ if (entry.isDirectory()) {
1805
+ await walkDir(fullPath, base, zip);
1806
+ } else {
1807
+ zip.addFileBuffer(relative(base, fullPath).replace(/\\/g, "/"), await readFile(fullPath));
1808
+ }
1809
+ }
1810
+ }
1694
1811
  function registerHostingTools(server, api) {
1695
1812
  server.registerTool(
1696
1813
  "hosting_deployments_list",
@@ -2106,6 +2223,73 @@ function registerHostingTools(server, api) {
2106
2223
  }
2107
2224
  }
2108
2225
  );
2226
+ server.registerTool(
2227
+ "hosting_deployments_upload_directory",
2228
+ {
2229
+ description: "Upload a local directory as a ZIP archive to an existing deployment. Supports binary files (images, fonts, compiled assets). Use this instead of hosting_deployments_upload when deploying a built frontend from the local filesystem.\n\nTypical workflow:\n 1. hosting_deployments_create \u2192 get deploymentId\n 2. hosting_deployments_upload_directory \u2192 upload ./dist or any local folder\n 3. hosting_deployments_activate \u2192 make the deployment live",
2230
+ inputSchema: {
2231
+ projectId: z5.string().describe("Project ID"),
2232
+ deploymentId: z5.string().describe("Deployment ID (from hosting_deployments_create)"),
2233
+ directoryPath: z5.string().describe('Absolute or relative path to the local directory to upload (e.g. "./dist")')
2234
+ }
2235
+ },
2236
+ async ({ projectId, deploymentId, directoryPath }) => {
2237
+ try {
2238
+ const absDir = resolve(directoryPath);
2239
+ const cwd = process.cwd();
2240
+ if (absDir !== cwd && !absDir.startsWith(cwd + sep)) {
2241
+ return {
2242
+ content: [{ type: "text", text: `directoryPath must be within the current working directory (${cwd})` }],
2243
+ isError: true
2244
+ };
2245
+ }
2246
+ const stat = lstatSync(absDir, { throwIfNoEntry: false });
2247
+ if (!stat) {
2248
+ return {
2249
+ content: [{ type: "text", text: `Directory not found: ${absDir}` }],
2250
+ isError: true
2251
+ };
2252
+ }
2253
+ if (!stat.isDirectory()) {
2254
+ return {
2255
+ content: [{ type: "text", text: `Path is not a directory: ${absDir}` }],
2256
+ isError: true
2257
+ };
2258
+ }
2259
+ const zip = new ZipBuilder();
2260
+ await walkDir(absDir, absDir, zip);
2261
+ if (zip.fileCount === 0) {
2262
+ return {
2263
+ content: [{ type: "text", text: `Directory is empty: ${absDir}` }],
2264
+ isError: true
2265
+ };
2266
+ }
2267
+ const zipBuffer = zip.toBuffer();
2268
+ const result = await api.uploadFile(
2269
+ `/hosting/projects/${encodeURIComponent(projectId)}/deployments/${encodeURIComponent(deploymentId)}/upload`,
2270
+ zipBuffer,
2271
+ "site.zip"
2272
+ );
2273
+ const response = {
2274
+ success: true,
2275
+ directoryPath: absDir,
2276
+ bundleSizeBytes: zipBuffer.length
2277
+ };
2278
+ if (result && typeof result === "object") {
2279
+ Object.assign(response, result);
2280
+ }
2281
+ return {
2282
+ content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
2283
+ };
2284
+ } catch (error) {
2285
+ const message = error instanceof Error ? error.message : String(error);
2286
+ return {
2287
+ content: [{ type: "text", text: `Directory upload failed: ${message}` }],
2288
+ isError: true
2289
+ };
2290
+ }
2291
+ }
2292
+ );
2109
2293
  }
2110
2294
 
2111
2295
  // libs/mcp-server/src/tools/storage.ts
@@ -2428,7 +2612,10 @@ function registerFunctionTools(server, api) {
2428
2612
  cronTimezone: z7.string().optional(),
2429
2613
  timeout: z7.number().int().min(1e3).max(12e4).optional(),
2430
2614
  memoryLimitMb: z7.number().int().min(16).max(512).optional(),
2431
- enabled: z7.boolean().optional()
2615
+ enabled: z7.boolean().optional(),
2616
+ invokeMode: z7.enum(["webhook", "authenticated", "public", "hybrid"]).optional().describe(
2617
+ "Controls which credentials the public invoke endpoint accepts. webhook: X-Webhook-Secret only (default). authenticated: JWT only. public: no auth. hybrid: either; JWT wins on conflict."
2618
+ )
2432
2619
  }
2433
2620
  },
2434
2621
  async ({ projectId, ...body }) => {
@@ -2463,7 +2650,10 @@ function registerFunctionTools(server, api) {
2463
2650
  cronTimezone: z7.string().optional(),
2464
2651
  timeout: z7.number().int().min(1e3).max(12e4).optional(),
2465
2652
  memoryLimitMb: z7.number().int().min(16).max(512).optional(),
2466
- enabled: z7.boolean().optional()
2653
+ enabled: z7.boolean().optional(),
2654
+ invokeMode: z7.enum(["webhook", "authenticated", "public", "hybrid"]).optional().describe(
2655
+ "Controls which credentials the public invoke endpoint accepts. webhook: X-Webhook-Secret only. authenticated: JWT only. public: no auth. hybrid: either; JWT wins on conflict."
2656
+ )
2467
2657
  }
2468
2658
  },
2469
2659
  async ({ projectId, functionId, ...body }) => {