@spacelr/mcp 0.0.12 → 0.1.0
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 +226 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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,88 @@ 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, stream retention, write rate-limit override, or search filter requirements. `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. `writeThrottle` overrides the default per-collection write rate limit (10 writes / 10 s) \u2014 set `enabled: false` to disable entirely (e.g. audit logs), or override `limit` / `windowMs`. `searchConfig.requireFilter` declares top-level filter keys that callers MUST include in every `collection.search()` call to prevent unindexable full-scan regex queries \u2014 affects ALL callers immediately (no deprecation window). 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
|
+
writeThrottle: z4.object({
|
|
1244
|
+
enabled: z4.boolean().optional().describe("When false, skip write throttling entirely for this collection"),
|
|
1245
|
+
limit: z4.number().int().min(1).max(1e4).optional().describe("Max writes per window (1..10000)"),
|
|
1246
|
+
windowMs: z4.number().int().min(1e3).max(36e5).optional().describe("Window duration in ms (1000..3_600_000 = 1s to 1h)")
|
|
1247
|
+
}).refine(
|
|
1248
|
+
(t) => t.enabled !== void 0 || t.limit !== void 0 || t.windowMs !== void 0,
|
|
1249
|
+
{ message: "writeThrottle must set at least one of enabled, limit, or windowMs" }
|
|
1250
|
+
).optional(),
|
|
1251
|
+
searchConfig: z4.object({
|
|
1252
|
+
requireFilter: z4.array(
|
|
1253
|
+
z4.string().regex(/^[a-zA-Z0-9_]+$/, "Top-level field names only (no dots, alphanumeric + underscore)").max(64).refine(
|
|
1254
|
+
(k) => !["__proto__", "constructor", "prototype"].includes(k),
|
|
1255
|
+
{ message: "Reserved JS object keys (__proto__, constructor, prototype) are not allowed" }
|
|
1256
|
+
)
|
|
1257
|
+
).min(1).max(10).optional().describe(
|
|
1258
|
+
"Top-level filter keys required on every search() call. Empty array rejected \u2014 to CLEAR an existing requirement, pass `searchConfig: {}` (an empty object). Omitting `searchConfig` entirely is a no-op (no change). Duplicate keys are deduplicated server-side, but you should send a deduplicated list."
|
|
1259
|
+
)
|
|
1260
|
+
// `searchConfig.fields` is intentionally NOT exposed here. The
|
|
1261
|
+
// backend DTO accepts it but treats it as "Reserved: future
|
|
1262
|
+
// allow-list of searchable fields. Stored but not enforced.".
|
|
1263
|
+
// Surfacing it via MCP would let callers persist values that
|
|
1264
|
+
// have no observable effect today and may take on different
|
|
1265
|
+
// semantics when enforcement ships. Add back once the feature
|
|
1266
|
+
// is live.
|
|
1267
|
+
}).strict().optional()
|
|
1268
|
+
}
|
|
1269
|
+
},
|
|
1270
|
+
async ({ projectId, name, realtimeMode, streamRetention, writeThrottle, searchConfig }) => {
|
|
1271
|
+
try {
|
|
1272
|
+
const body = {};
|
|
1273
|
+
if (realtimeMode !== void 0) body.realtimeMode = realtimeMode;
|
|
1274
|
+
if (streamRetention !== void 0) body.streamRetention = streamRetention;
|
|
1275
|
+
if (writeThrottle !== void 0) body.writeThrottle = writeThrottle;
|
|
1276
|
+
if (searchConfig !== void 0) {
|
|
1277
|
+
const normalized = {};
|
|
1278
|
+
if (searchConfig.requireFilter !== void 0) {
|
|
1279
|
+
normalized.requireFilter = Array.from(new Set(searchConfig.requireFilter));
|
|
1280
|
+
}
|
|
1281
|
+
body.searchConfig = normalized;
|
|
1282
|
+
}
|
|
1283
|
+
if (Object.keys(body).length === 0) {
|
|
1284
|
+
return {
|
|
1285
|
+
content: [
|
|
1286
|
+
{
|
|
1287
|
+
type: "text",
|
|
1288
|
+
text: "At least one of realtimeMode, streamRetention, writeThrottle, or searchConfig must be provided"
|
|
1289
|
+
}
|
|
1290
|
+
],
|
|
1291
|
+
isError: true
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
const result = await api.patch(
|
|
1295
|
+
`/databases/${encodeURIComponent(projectId)}/collections/${encodeURIComponent(name)}`,
|
|
1296
|
+
{ body }
|
|
1297
|
+
);
|
|
1298
|
+
return {
|
|
1299
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1300
|
+
};
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1303
|
+
return {
|
|
1304
|
+
content: [{ type: "text", text: `Failed to update collection metadata: ${message}` }],
|
|
1305
|
+
isError: true
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
);
|
|
1181
1310
|
server.registerTool(
|
|
1182
1311
|
"database_collections_delete",
|
|
1183
1312
|
{
|
|
@@ -1592,6 +1721,9 @@ function registerDatabaseTools(server, api) {
|
|
|
1592
1721
|
}
|
|
1593
1722
|
|
|
1594
1723
|
// libs/mcp-server/src/tools/hosting.ts
|
|
1724
|
+
import { readdir, readFile } from "fs/promises";
|
|
1725
|
+
import { lstatSync } from "fs";
|
|
1726
|
+
import { join as join2, relative, resolve, sep } from "path";
|
|
1595
1727
|
import { z as z5 } from "zod";
|
|
1596
1728
|
|
|
1597
1729
|
// libs/mcp-server/src/zip.ts
|
|
@@ -1607,17 +1739,29 @@ var ZipBuilder = class {
|
|
|
1607
1739
|
* @param content - UTF-8 text content of the file
|
|
1608
1740
|
*/
|
|
1609
1741
|
addFile(name, content) {
|
|
1742
|
+
this.addFileBuffer(name, Buffer.from(content, "utf-8"));
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Add a binary file to the archive.
|
|
1746
|
+
*
|
|
1747
|
+
* @param name - File path inside the ZIP (e.g. "images/logo.png")
|
|
1748
|
+
* @param content - Raw file content as a Buffer
|
|
1749
|
+
*/
|
|
1750
|
+
addFileBuffer(name, content) {
|
|
1610
1751
|
const nameBuffer = Buffer.from(name, "utf-8");
|
|
1611
|
-
const
|
|
1612
|
-
const
|
|
1613
|
-
const crc32 = computeCrc32(contentBuffer);
|
|
1752
|
+
const compressed = deflateRawSync(content, { level: 9 });
|
|
1753
|
+
const crc32 = computeCrc32(content);
|
|
1614
1754
|
this.files.push({
|
|
1615
1755
|
name: nameBuffer,
|
|
1616
|
-
content
|
|
1756
|
+
content,
|
|
1617
1757
|
compressed,
|
|
1618
1758
|
crc32
|
|
1619
1759
|
});
|
|
1620
1760
|
}
|
|
1761
|
+
/** Number of files added to the archive. */
|
|
1762
|
+
get fileCount() {
|
|
1763
|
+
return this.files.length;
|
|
1764
|
+
}
|
|
1621
1765
|
/** Generate the complete ZIP archive as a Buffer. */
|
|
1622
1766
|
toBuffer() {
|
|
1623
1767
|
const localHeaders = [];
|
|
@@ -1691,6 +1835,17 @@ function computeCrc32(data) {
|
|
|
1691
1835
|
}
|
|
1692
1836
|
|
|
1693
1837
|
// libs/mcp-server/src/tools/hosting.ts
|
|
1838
|
+
async function walkDir(dir, base, zip) {
|
|
1839
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
1840
|
+
const fullPath = join2(dir, entry.name);
|
|
1841
|
+
if (entry.isSymbolicLink()) continue;
|
|
1842
|
+
if (entry.isDirectory()) {
|
|
1843
|
+
await walkDir(fullPath, base, zip);
|
|
1844
|
+
} else {
|
|
1845
|
+
zip.addFileBuffer(relative(base, fullPath).replace(/\\/g, "/"), await readFile(fullPath));
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1694
1849
|
function registerHostingTools(server, api) {
|
|
1695
1850
|
server.registerTool(
|
|
1696
1851
|
"hosting_deployments_list",
|
|
@@ -2106,6 +2261,73 @@ function registerHostingTools(server, api) {
|
|
|
2106
2261
|
}
|
|
2107
2262
|
}
|
|
2108
2263
|
);
|
|
2264
|
+
server.registerTool(
|
|
2265
|
+
"hosting_deployments_upload_directory",
|
|
2266
|
+
{
|
|
2267
|
+
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",
|
|
2268
|
+
inputSchema: {
|
|
2269
|
+
projectId: z5.string().describe("Project ID"),
|
|
2270
|
+
deploymentId: z5.string().describe("Deployment ID (from hosting_deployments_create)"),
|
|
2271
|
+
directoryPath: z5.string().describe('Absolute or relative path to the local directory to upload (e.g. "./dist")')
|
|
2272
|
+
}
|
|
2273
|
+
},
|
|
2274
|
+
async ({ projectId, deploymentId, directoryPath }) => {
|
|
2275
|
+
try {
|
|
2276
|
+
const absDir = resolve(directoryPath);
|
|
2277
|
+
const cwd = process.cwd();
|
|
2278
|
+
if (absDir !== cwd && !absDir.startsWith(cwd + sep)) {
|
|
2279
|
+
return {
|
|
2280
|
+
content: [{ type: "text", text: `directoryPath must be within the current working directory (${cwd})` }],
|
|
2281
|
+
isError: true
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
const stat = lstatSync(absDir, { throwIfNoEntry: false });
|
|
2285
|
+
if (!stat) {
|
|
2286
|
+
return {
|
|
2287
|
+
content: [{ type: "text", text: `Directory not found: ${absDir}` }],
|
|
2288
|
+
isError: true
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
if (!stat.isDirectory()) {
|
|
2292
|
+
return {
|
|
2293
|
+
content: [{ type: "text", text: `Path is not a directory: ${absDir}` }],
|
|
2294
|
+
isError: true
|
|
2295
|
+
};
|
|
2296
|
+
}
|
|
2297
|
+
const zip = new ZipBuilder();
|
|
2298
|
+
await walkDir(absDir, absDir, zip);
|
|
2299
|
+
if (zip.fileCount === 0) {
|
|
2300
|
+
return {
|
|
2301
|
+
content: [{ type: "text", text: `Directory is empty: ${absDir}` }],
|
|
2302
|
+
isError: true
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
const zipBuffer = zip.toBuffer();
|
|
2306
|
+
const result = await api.uploadFile(
|
|
2307
|
+
`/hosting/projects/${encodeURIComponent(projectId)}/deployments/${encodeURIComponent(deploymentId)}/upload`,
|
|
2308
|
+
zipBuffer,
|
|
2309
|
+
"site.zip"
|
|
2310
|
+
);
|
|
2311
|
+
const response = {
|
|
2312
|
+
success: true,
|
|
2313
|
+
directoryPath: absDir,
|
|
2314
|
+
bundleSizeBytes: zipBuffer.length
|
|
2315
|
+
};
|
|
2316
|
+
if (result && typeof result === "object") {
|
|
2317
|
+
Object.assign(response, result);
|
|
2318
|
+
}
|
|
2319
|
+
return {
|
|
2320
|
+
content: [{ type: "text", text: JSON.stringify(response, null, 2) }]
|
|
2321
|
+
};
|
|
2322
|
+
} catch (error) {
|
|
2323
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2324
|
+
return {
|
|
2325
|
+
content: [{ type: "text", text: `Directory upload failed: ${message}` }],
|
|
2326
|
+
isError: true
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
);
|
|
2109
2331
|
}
|
|
2110
2332
|
|
|
2111
2333
|
// libs/mcp-server/src/tools/storage.ts
|