@sakupa/mcp 0.1.0 → 0.3.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/bin.js +188 -211
- package/dist/index.js +240 -263
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -136,11 +136,11 @@ var HTTP_STATUS = {
|
|
|
136
136
|
conflict: 409,
|
|
137
137
|
state_conflict: 409,
|
|
138
138
|
rate_limited: 429,
|
|
139
|
-
insufficient_balance: 402,
|
|
140
139
|
payment_required: 402,
|
|
141
140
|
confirmation_required: 428,
|
|
142
141
|
dns_not_verified: 403,
|
|
143
142
|
not_deployable: 422,
|
|
143
|
+
upgrade_required: 426,
|
|
144
144
|
internal: 500
|
|
145
145
|
};
|
|
146
146
|
var SakupaError = class extends Error {
|
|
@@ -440,12 +440,22 @@ function safeDecode(bytes) {
|
|
|
440
440
|
}
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
+
// ../core/dist/domain/hashing.js
|
|
444
|
+
async function sha256Hex(bytes) {
|
|
445
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
|
|
446
|
+
let hex = "";
|
|
447
|
+
for (const byte of new Uint8Array(digest))
|
|
448
|
+
hex += byte.toString(16).padStart(2, "0");
|
|
449
|
+
return hex;
|
|
450
|
+
}
|
|
451
|
+
|
|
443
452
|
// ../core/dist/domain/subscription.js
|
|
444
|
-
var SUBSCRIPTION_WARNING_TEXT = "You are subscribing {
|
|
453
|
+
var SUBSCRIPTION_WARNING_TEXT = "You are subscribing this site ({siteUrl}) to Sakupa Hosting: the {plan} monthly plan (\xA5{priceJpy}/month).\n\nPaying makes THIS site permanent on its {siteUrl} address \u2014 it stops expiring. Binding a custom domain afterwards is an optional included extra: it requires proving DNS control of that domain, and whether or not you ever bind one does not change the subscription or qualify for a refund.\n\nIf this site outgrows its plan, Sakupa automatically upgrades the subscription to the next plan (water -> personal -> share -> business) and renewals bill the new plan. There is no metered overage: above the Business plan, growth is limited instead of billed further.\n\nYou can cancel anytime; when the subscription ends, the site immediately becomes a free temporary site again (24h validity) and all paid data \u2014 custom domain bindings included \u2014 is removed.";
|
|
445
454
|
|
|
446
455
|
// ../core/dist/dto.js
|
|
447
456
|
var CREDENTIAL_HEADER = "x-sakupa-credential";
|
|
448
457
|
var IDEMPOTENCY_HEADER = "x-sakupa-idempotency-key";
|
|
458
|
+
var MCP_VERSION_HEADER = "x-sakupa-mcp-version";
|
|
449
459
|
|
|
450
460
|
// ../core/dist/services/sites.js
|
|
451
461
|
var utf8Decoder = new TextDecoder("utf-8", { fatal: false });
|
|
@@ -464,11 +474,11 @@ var KNOWN_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
464
474
|
"conflict",
|
|
465
475
|
"state_conflict",
|
|
466
476
|
"rate_limited",
|
|
467
|
-
"insufficient_balance",
|
|
468
477
|
"payment_required",
|
|
469
478
|
"confirmation_required",
|
|
470
479
|
"dns_not_verified",
|
|
471
480
|
"not_deployable",
|
|
481
|
+
"upgrade_required",
|
|
472
482
|
"internal"
|
|
473
483
|
]);
|
|
474
484
|
var HttpApiClient = class {
|
|
@@ -569,14 +579,7 @@ var HttpApiClient = class {
|
|
|
569
579
|
{ body: req }
|
|
570
580
|
);
|
|
571
581
|
}
|
|
572
|
-
/**
|
|
573
|
-
async createCheckout(req) {
|
|
574
|
-
return this.call("POST", "/v1/payments/checkout", {
|
|
575
|
-
body: req,
|
|
576
|
-
idempotencyKey: req.idempotencyKey
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
/** Subscription checkout: a fixed monthly plan for one apex domain (owner-only). */
|
|
582
|
+
/** Subscription checkout: a fixed monthly plan permanentizing one site (owner-only). */
|
|
580
583
|
async createPlanCheckout(req, credential) {
|
|
581
584
|
return this.call("POST", "/v1/payments/checkout", {
|
|
582
585
|
credential,
|
|
@@ -585,31 +588,24 @@ var HttpApiClient = class {
|
|
|
585
588
|
});
|
|
586
589
|
}
|
|
587
590
|
/** Stripe-hosted billing portal (payment method, invoices, cancellation). */
|
|
588
|
-
async createBillingPortal(
|
|
591
|
+
async createBillingPortal(siteId, credential) {
|
|
589
592
|
return this.call(
|
|
590
593
|
"POST",
|
|
591
|
-
`/v1/
|
|
594
|
+
`/v1/sites/${encodeURIComponent(siteId)}/billing/portal`,
|
|
592
595
|
{ credential, body: {} }
|
|
593
596
|
);
|
|
594
597
|
}
|
|
595
|
-
async getBillingStatus(
|
|
598
|
+
async getBillingStatus(siteId, credential) {
|
|
596
599
|
return this.call(
|
|
597
600
|
"GET",
|
|
598
|
-
`/v1/
|
|
599
|
-
|
|
600
|
-
);
|
|
601
|
-
}
|
|
602
|
-
async setBillingPlan(apexDomain, credential, req) {
|
|
603
|
-
return this.call(
|
|
604
|
-
"POST",
|
|
605
|
-
`/v1/billing/${encodeURIComponent(apexDomain)}/plan`,
|
|
606
|
-
{ credential, body: req }
|
|
601
|
+
`/v1/sites/${encodeURIComponent(siteId)}/billing`,
|
|
602
|
+
{ credential }
|
|
607
603
|
);
|
|
608
604
|
}
|
|
609
|
-
async
|
|
605
|
+
async setBillingPlan(siteId, credential, req) {
|
|
610
606
|
return this.call(
|
|
611
607
|
"POST",
|
|
612
|
-
`/v1/sites/${encodeURIComponent(siteId)}/
|
|
608
|
+
`/v1/sites/${encodeURIComponent(siteId)}/billing/plan`,
|
|
613
609
|
{ credential, body: req }
|
|
614
610
|
);
|
|
615
611
|
}
|
|
@@ -1087,8 +1083,7 @@ function readSiteFile(projectDir) {
|
|
|
1087
1083
|
apiBaseUrl: typeof parsed.apiBaseUrl === "string" ? parsed.apiBaseUrl : "",
|
|
1088
1084
|
...typeof parsed.shortId === "string" ? { shortId: parsed.shortId } : {},
|
|
1089
1085
|
...typeof parsed.url === "string" ? { url: parsed.url } : {},
|
|
1090
|
-
...typeof parsed.
|
|
1091
|
-
...typeof parsed.billingDomainId === "string" ? { billingDomainId: parsed.billingDomainId } : {}
|
|
1086
|
+
...typeof parsed.boundDomain === "string" ? { boundDomain: parsed.boundDomain } : {}
|
|
1092
1087
|
};
|
|
1093
1088
|
} catch {
|
|
1094
1089
|
return null;
|
|
@@ -1107,7 +1102,7 @@ function writeSiteFile(projectDir, file) {
|
|
|
1107
1102
|
}
|
|
1108
1103
|
|
|
1109
1104
|
// src/version.ts
|
|
1110
|
-
var MCP_VERSION = "0.
|
|
1105
|
+
var MCP_VERSION = "0.3.0";
|
|
1111
1106
|
var CLIENT_TYPE = "sakupa-mcp";
|
|
1112
1107
|
|
|
1113
1108
|
// src/tools/context.ts
|
|
@@ -1147,8 +1142,8 @@ var severityEnum = z.enum(["low", "medium", "high", "critical"]);
|
|
|
1147
1142
|
function planCatalog() {
|
|
1148
1143
|
return TIER_ORDER.map((p) => `${p} \xA5${tierPriceJpy(p)}/month`).join(", ");
|
|
1149
1144
|
}
|
|
1150
|
-
function subscriptionWarning(
|
|
1151
|
-
return SUBSCRIPTION_WARNING_TEXT.replaceAll("{
|
|
1145
|
+
function subscriptionWarning(siteUrl, plan) {
|
|
1146
|
+
return SUBSCRIPTION_WARNING_TEXT.replaceAll("{siteUrl}", siteUrl).replaceAll("{plan}", plan).replaceAll("{priceJpy}", String(tierPriceJpy(plan)));
|
|
1152
1147
|
}
|
|
1153
1148
|
var ticketCategoryEnum = z.enum([
|
|
1154
1149
|
"billing",
|
|
@@ -1187,6 +1182,36 @@ Please ask the user to choose, then re-run deploy_site with:
|
|
|
1187
1182
|
Output directory: "${analysis.recommendedOutputDir ?? "."}", ${analysis.fileCount} files.`
|
|
1188
1183
|
);
|
|
1189
1184
|
}
|
|
1185
|
+
var MB2 = 1024 * 1024;
|
|
1186
|
+
function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
|
|
1187
|
+
const oversized = manifest.find((f) => f.size > MAX_SINGLE_FILE_BYTES);
|
|
1188
|
+
if (oversized) {
|
|
1189
|
+
throw new SakupaError(
|
|
1190
|
+
"validation_failed",
|
|
1191
|
+
`"${oversized.path}" is ${(oversized.size / MB2).toFixed(1)}MB, over the ${Math.floor(MAX_SINGLE_FILE_BYTES / MB2)}MB per-file limit. Remove or shrink it before deploying.`
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
if (isFirstFreeDeploy) {
|
|
1195
|
+
const total = manifest.reduce((sum, f) => sum + f.size, 0);
|
|
1196
|
+
if (total > FREE_SITE_MAX_TOTAL_BYTES) {
|
|
1197
|
+
throw new SakupaError(
|
|
1198
|
+
"validation_failed",
|
|
1199
|
+
`The static output is ${(total / MB2).toFixed(1)}MB, over the ${Math.floor(FREE_SITE_MAX_TOTAL_BYTES / MB2)}MB free-site limit. Reduce the output (bundle/compress assets, drop large media) or subscribe the site to a paid plan for larger sites.`
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
async function buildHashedManifest(files, outputAbs) {
|
|
1205
|
+
const manifest = [];
|
|
1206
|
+
for (const file of files) {
|
|
1207
|
+
const bytes = new Uint8Array(await fs2.readFile(join3(outputAbs, file.path)));
|
|
1208
|
+
manifest.push({ path: file.path, size: file.size, contentHash: await sha256Hex(bytes) });
|
|
1209
|
+
}
|
|
1210
|
+
return manifest;
|
|
1211
|
+
}
|
|
1212
|
+
function isIncrementalReuseFailure(e) {
|
|
1213
|
+
return isSakupaError(e) && e.code === "state_conflict" && typeof e.details === "object" && e.details !== null && e.details.incrementalReuseFailed === true;
|
|
1214
|
+
}
|
|
1190
1215
|
async function uploadAll(ctx, targets, files, outputAbs) {
|
|
1191
1216
|
for (let i = 0; i < targets.length; i++) {
|
|
1192
1217
|
const target = targets[i];
|
|
@@ -1199,6 +1224,12 @@ async function uploadAll(ctx, targets, files, outputAbs) {
|
|
|
1199
1224
|
);
|
|
1200
1225
|
}
|
|
1201
1226
|
const bytes = new Uint8Array(await fs2.readFile(join3(outputAbs, match.path)));
|
|
1227
|
+
if (bytes.byteLength !== match.size) {
|
|
1228
|
+
throw new SakupaError(
|
|
1229
|
+
"validation_failed",
|
|
1230
|
+
`"${match.path}" changed since analysis (${match.size} -> ${bytes.byteLength} bytes). Re-run deploy_site so Sakupa re-checks the current files before uploading.`
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1202
1233
|
await ctx.client.uploadFile(target, bytes);
|
|
1203
1234
|
}
|
|
1204
1235
|
return targets.length;
|
|
@@ -1234,7 +1265,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1234
1265
|
server.registerTool(
|
|
1235
1266
|
"deploy_site",
|
|
1236
1267
|
{
|
|
1237
|
-
description: `Deploy the local static output to Sakupa. First deploy creates a free temporary site (valid ${FREE_SITE_TTL_HOURS}h, public URL like https://{shortId}.sakupa.com) and stores the management credential in .sakupa/site.json. Later runs update the existing site
|
|
1268
|
+
description: `Deploy the local static output to Sakupa. First deploy creates a free temporary site (valid ${FREE_SITE_TTL_HOURS}h, public URL like https://{shortId}.sakupa.com) and stores the management credential in .sakupa/site.json. Later runs update the existing site (free sites also refresh their validity; subscribed sites are permanent). Runs analyze_site first and refuses to upload source projects, secrets, .env files, archives, media or server code. Never uploads anything when the analysis says the project is not deployable.`,
|
|
1238
1269
|
inputSchema: {
|
|
1239
1270
|
outputDir: z.string().optional().describe("Output directory relative to the project root (overrides detection)."),
|
|
1240
1271
|
spaFallback: z.boolean().optional().describe("Enable SPA fallback (requires spaFallbackConfirmed: true)."),
|
|
@@ -1256,9 +1287,10 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1256
1287
|
return notDeployableResult(analysis);
|
|
1257
1288
|
}
|
|
1258
1289
|
const files = analysis.files;
|
|
1259
|
-
const manifest = files.map((f) => ({ path: f.path, size: f.size }));
|
|
1260
1290
|
const outputAbs = resolve2(ctx.projectDir, analysis.recommendedOutputDir ?? ".");
|
|
1291
|
+
const manifest = await buildHashedManifest(files, outputAbs);
|
|
1261
1292
|
const existing = readSiteFile(ctx.projectDir);
|
|
1293
|
+
ensureUploadSizeWithinLimits(manifest, !existing);
|
|
1262
1294
|
if (!existing) {
|
|
1263
1295
|
const created = await ctx.client.createSite({
|
|
1264
1296
|
manifest,
|
|
@@ -1285,32 +1317,49 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1285
1317
|
Files uploaded: ${uploaded2} (${finalized2.totalBytes} bytes)
|
|
1286
1318
|
` + (finalized2.expiresAt ? `Expires at: ${finalized2.expiresAt}
|
|
1287
1319
|
` : "") + `
|
|
1288
|
-
This is a FREE temporary preview: it stays live for ${FREE_SITE_TTL_HOURS} hours. Deploying again or calling refresh_site extends the validity. The management credential was saved to .sakupa/site.json \u2014 keep that file: it is the only way to manage this
|
|
1320
|
+
This is a FREE temporary preview: it stays live for ${FREE_SITE_TTL_HOURS} hours. Deploying again or calling refresh_site extends the validity; subscribing the site (subscribe_site) makes this URL permanent. The management credential was saved to .sakupa/site.json \u2014 keep that file: it is the only way to manage this site.
|
|
1289
1321
|
` + (finalized2.warnings.length > 0 ? `
|
|
1290
1322
|
Warnings:
|
|
1291
1323
|
${JSON.stringify(finalized2.warnings, null, 2)}` : "")
|
|
1292
1324
|
);
|
|
1293
1325
|
}
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
deployment.
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1326
|
+
const updateOnce = async (forceFullUpload) => {
|
|
1327
|
+
const req = {
|
|
1328
|
+
manifest,
|
|
1329
|
+
...args.lang !== void 0 ? { lang: args.lang } : {},
|
|
1330
|
+
spaFallback: args.spaFallback === true,
|
|
1331
|
+
...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {},
|
|
1332
|
+
...forceFullUpload ? { forceFullUpload: true } : {}
|
|
1333
|
+
};
|
|
1334
|
+
const deployment = await ctx.client.createDeployment(
|
|
1335
|
+
existing.siteId,
|
|
1336
|
+
existing.credential,
|
|
1337
|
+
req
|
|
1338
|
+
);
|
|
1339
|
+
const uploaded2 = await uploadAll(ctx, deployment.uploadTargets, files, outputAbs);
|
|
1340
|
+
const finalized2 = await ctx.client.finalizeDeployment(
|
|
1341
|
+
deployment.deploymentId,
|
|
1342
|
+
existing.siteId,
|
|
1343
|
+
existing.credential
|
|
1344
|
+
);
|
|
1345
|
+
return { uploaded: uploaded2, finalized: finalized2 };
|
|
1346
|
+
};
|
|
1347
|
+
let update;
|
|
1348
|
+
try {
|
|
1349
|
+
update = await updateOnce(false);
|
|
1350
|
+
} catch (e) {
|
|
1351
|
+
if (!isIncrementalReuseFailure(e)) throw e;
|
|
1352
|
+
update = await updateOnce(true);
|
|
1353
|
+
}
|
|
1354
|
+
const { uploaded, finalized } = update;
|
|
1306
1355
|
writeSiteFile(ctx.projectDir, { ...existing, url: finalized.url });
|
|
1307
1356
|
return text(
|
|
1308
1357
|
`Site updated: ${finalized.url}
|
|
1309
1358
|
Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
|
|
1310
1359
|
` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
|
|
1311
1360
|
` : "") + (finalized.mode === "free" ? `
|
|
1312
|
-
Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh_site call.
|
|
1313
|
-
` : "") + (finalized.warnings.length > 0 ? `
|
|
1361
|
+
Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh_site call. Subscribing (subscribe_site) makes the site permanent.
|
|
1362
|
+
` : "\nThis site is subscribed and permanent \u2014 no expiry.\n") + (finalized.warnings.length > 0 ? `
|
|
1314
1363
|
Warnings:
|
|
1315
1364
|
${JSON.stringify(finalized.warnings, null, 2)}` : "")
|
|
1316
1365
|
);
|
|
@@ -1322,7 +1371,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : "")
|
|
|
1322
1371
|
server.registerTool(
|
|
1323
1372
|
"refresh_site",
|
|
1324
1373
|
{
|
|
1325
|
-
description: "Refresh the validity of the free temporary site WITHOUT uploading content. Uses the local credential in .sakupa/site.json.",
|
|
1374
|
+
description: "Refresh the validity of the free temporary site WITHOUT uploading content. Uses the local credential in .sakupa/site.json. Subscribed sites are permanent and need no refresh.",
|
|
1326
1375
|
inputSchema: {}
|
|
1327
1376
|
},
|
|
1328
1377
|
async () => {
|
|
@@ -1341,7 +1390,7 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1341
1390
|
server.registerTool(
|
|
1342
1391
|
"site_status",
|
|
1343
1392
|
{
|
|
1344
|
-
description: "Show the current status of this project's Sakupa site: URL, mode (free/paid), expiry,
|
|
1393
|
+
description: "Show the current status of this project's Sakupa site: URL, mode (free/paid), expiry, subscription state, custom domains, size, last deployment and warnings.",
|
|
1345
1394
|
inputSchema: {}
|
|
1346
1395
|
},
|
|
1347
1396
|
async () => {
|
|
@@ -1354,10 +1403,53 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1354
1403
|
}
|
|
1355
1404
|
}
|
|
1356
1405
|
);
|
|
1406
|
+
server.registerTool(
|
|
1407
|
+
"subscribe_site",
|
|
1408
|
+
{
|
|
1409
|
+
description: `Create a Stripe Checkout link that subscribes THIS site to a Sakupa Hosting monthly plan (${planCatalog()}). Paying makes the site PERMANENT on its {shortId}.sakupa.com URL \u2014 no more 24h expiry; that is the core value of paying. Binding a custom domain afterwards (bind_domain) is an optional included extra and requires DNS control of that domain. Owner-only: requires this project's site credential (.sakupa/site.json) \u2014 deploy_site first. If the site outgrows its plan, Sakupa auto-upgrades to the next plan (capped at Business, where growth is limited instead of billed further). Card details are entered only on the Stripe-hosted page \u2014 never through the AI tool. Without confirm: true this tool only shows the mandatory disclosure and makes no API call.`,
|
|
1410
|
+
inputSchema: {
|
|
1411
|
+
plan: planEnum.describe(
|
|
1412
|
+
"Monthly plan: water (very light personal pages), personal (personal brand / small shop), share (small-business site), business (steadier traffic, more headroom)."
|
|
1413
|
+
),
|
|
1414
|
+
confirm: z.boolean().optional().describe("User read the subscription disclosure and confirmed. Required to proceed.")
|
|
1415
|
+
}
|
|
1416
|
+
},
|
|
1417
|
+
async (args) => {
|
|
1418
|
+
try {
|
|
1419
|
+
const site = requireSiteFile(ctx);
|
|
1420
|
+
const siteUrl = site.url ?? (site.shortId ? `https://${site.shortId}.sakupa.com` : site.siteId);
|
|
1421
|
+
if (args.confirm !== true) {
|
|
1422
|
+
return text(
|
|
1423
|
+
`${subscriptionWarning(siteUrl, args.plan)}
|
|
1424
|
+
|
|
1425
|
+
No checkout link was created yet. Please show this disclosure to the user and, after their explicit confirmation, re-run subscribe_site with confirm: true.`
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
const res = await ctx.client.createPlanCheckout(
|
|
1429
|
+
{
|
|
1430
|
+
siteId: site.siteId,
|
|
1431
|
+
plan: args.plan,
|
|
1432
|
+
idempotencyKey: randomUUID(),
|
|
1433
|
+
confirmPlan: true
|
|
1434
|
+
},
|
|
1435
|
+
site.credential
|
|
1436
|
+
);
|
|
1437
|
+
return text(
|
|
1438
|
+
`Stripe Checkout link \u2014 Sakupa Hosting for this site: ${res.plan} plan, \xA5${res.monthlyPriceJpy}/month
|
|
1439
|
+
${res.checkoutUrl}
|
|
1440
|
+
|
|
1441
|
+
Open this link in a browser to subscribe. Card data is entered only on the Stripe-hosted page \u2014 never give card numbers, passwords or security codes to the AI tool.
|
|
1442
|
+
Once payment confirms, the site becomes permanent on its current URL. Binding a custom domain (bind_domain) is optional and still requires DNS verification.`
|
|
1443
|
+
);
|
|
1444
|
+
} catch (e) {
|
|
1445
|
+
return toolError(e);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
);
|
|
1357
1449
|
server.registerTool(
|
|
1358
1450
|
"bind_domain",
|
|
1359
1451
|
{
|
|
1360
|
-
description:
|
|
1452
|
+
description: "Bind a custom domain to this subscribed site \u2014 an OPTIONAL extra serving surface; the permanent {shortId}.sakupa.com URL keeps working alongside it. Requires an ACTIVE subscription (subscribe_site). Ownership is proven ONLY by DNS control of the registered apex domain \u2014 payment never grants ownership. A binding request never reserves the hostname: whoever proves DNS control first gets it, and unverified requests expire after 72 hours. Call again with verificationId to check progress.",
|
|
1361
1453
|
inputSchema: {
|
|
1362
1454
|
hostname: z.string().describe('Hostname to bind, e.g. "example.com" or "www.example.com".'),
|
|
1363
1455
|
verificationId: z.string().optional().describe("Check an existing DNS verification instead of starting a new one.")
|
|
@@ -1369,12 +1461,12 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1369
1461
|
if (args.verificationId !== void 0) {
|
|
1370
1462
|
const res2 = await ctx.client.checkVerification(args.verificationId, site.credential);
|
|
1371
1463
|
if (res2.status === "verified") {
|
|
1372
|
-
writeSiteFile(ctx.projectDir, { ...site,
|
|
1464
|
+
writeSiteFile(ctx.projectDir, { ...site, boundDomain: res2.apexDomain });
|
|
1373
1465
|
}
|
|
1374
1466
|
return text(
|
|
1375
1467
|
`DNS verification ${res2.verificationId}: ${res2.status}
|
|
1376
1468
|
${res2.message}
|
|
1377
|
-
` + (res2.provisioningJobId ? `Provisioning started (job ${res2.provisioningJobId}). HTTPS certificate and
|
|
1469
|
+
` + (res2.provisioningJobId ? `Provisioning started (job ${res2.provisioningJobId}). HTTPS certificate and serving setup are in progress; check again with bind_domain + verificationId later.
|
|
1378
1470
|
` : "") + (res2.pendingDnsRecords.length > 0 ? `
|
|
1379
1471
|
DNS records still required:
|
|
1380
1472
|
${JSON.stringify(res2.pendingDnsRecords, null, 2)}` : "")
|
|
@@ -1385,22 +1477,15 @@ ${JSON.stringify(res2.pendingDnsRecords, null, 2)}` : "")
|
|
|
1385
1477
|
hostname: args.hostname
|
|
1386
1478
|
};
|
|
1387
1479
|
const res = await ctx.client.bindDomain(site.credential, req);
|
|
1388
|
-
writeSiteFile(ctx.projectDir, {
|
|
1389
|
-
...site,
|
|
1390
|
-
billingDomain: res.apexDomain,
|
|
1391
|
-
billingDomainId: res.billingDomainId
|
|
1392
|
-
});
|
|
1393
1480
|
const servingHint = res.isApex ? `"${res.hostname}" is an apex domain: your DNS provider must support ALIAS, ANAME, CNAME flattening, or alias records to point it at the assigned CDN domain.` : `"${res.hostname}" is a subdomain: create a CNAME record pointing it at the assigned CDN domain once provisioning completes.`;
|
|
1394
1481
|
return text(
|
|
1395
1482
|
`Domain binding started for ${res.hostname}.
|
|
1396
1483
|
|
|
1397
|
-
Billing subject: the apex domain "${res.apexDomain}". Subdomains of ${res.apexDomain} cannot be separate billing subjects \u2014 they all share this domain hosting subscription.
|
|
1398
|
-
|
|
1399
1484
|
1. Prove control of ${res.apexDomain} by creating this DNS record:
|
|
1400
1485
|
name: ${res.verificationRecord.name}
|
|
1401
1486
|
type: ${res.verificationRecord.type}
|
|
1402
1487
|
value: ${res.verificationRecord.value}
|
|
1403
|
-
Ownership
|
|
1488
|
+
Ownership comes ONLY from DNS control; paying never grants it. This request does not reserve the hostname \u2014 the first verified request wins, and this challenge expires after 72 hours.
|
|
1404
1489
|
|
|
1405
1490
|
2. Serving DNS (after verification): ${servingHint}
|
|
1406
1491
|
Server guidance: ${res.servingInstructions}
|
|
@@ -1415,33 +1500,24 @@ Then run bind_domain again with verificationId: "${res.verificationId}" to check
|
|
|
1415
1500
|
server.registerTool(
|
|
1416
1501
|
"billing_status",
|
|
1417
1502
|
{
|
|
1418
|
-
description: "Show
|
|
1419
|
-
inputSchema: {
|
|
1420
|
-
domain: z.string().optional().describe("Apex billing domain; defaults to the domain bound in .sakupa/site.json.")
|
|
1421
|
-
}
|
|
1503
|
+
description: "Show this site's hosting subscription: plan, payment state, current paid period, reconciled usage, estimated usage tier, bound custom domains and risks. Owner-only (uses the credential in .sakupa/site.json).",
|
|
1504
|
+
inputSchema: {}
|
|
1422
1505
|
},
|
|
1423
|
-
async (
|
|
1506
|
+
async () => {
|
|
1424
1507
|
try {
|
|
1425
|
-
const site =
|
|
1426
|
-
const
|
|
1427
|
-
if (!domain) {
|
|
1428
|
-
throw new SakupaError(
|
|
1429
|
-
"invalid_request",
|
|
1430
|
-
'No domain given and this project has no bound billing domain. Pass domain, e.g. billing_status { domain: "example.com" }.'
|
|
1431
|
-
);
|
|
1432
|
-
}
|
|
1433
|
-
const res = await ctx.client.getBillingStatus(domain, site?.credential);
|
|
1508
|
+
const site = requireSiteFile(ctx);
|
|
1509
|
+
const res = await ctx.client.getBillingStatus(site.siteId, site.credential);
|
|
1434
1510
|
const lines = [
|
|
1435
|
-
`Billing status for ${res.
|
|
1436
|
-
res.
|
|
1511
|
+
`Billing status for site ${res.siteId} (mode: ${res.mode})`,
|
|
1512
|
+
res.permanentUrl ? `Permanent URL: ${res.permanentUrl}` : void 0,
|
|
1513
|
+
res.plan ? `Plan: ${res.plan} (\xA5${res.monthlyPriceJpy ?? tierPriceJpy(res.plan)}/month)` : "Plan: (no subscription yet)",
|
|
1437
1514
|
res.subscriptionStatus ? `Subscription payment state: ${res.subscriptionStatus}` : void 0,
|
|
1438
|
-
res.
|
|
1439
|
-
res.
|
|
1515
|
+
res.cancelAtPeriodEnd ? "Renewal: CANCELED \u2014 the site reverts to free at the end of the already-paid month" : void 0,
|
|
1516
|
+
res.currentPeriodStart ? `Current paid period: ${res.currentPeriodStart} -> ${res.currentPeriodEnd ?? "?"}` : void 0,
|
|
1440
1517
|
res.estimatedUsageTier ? `Estimated usage tier: ${res.estimatedUsageTier}` : void 0,
|
|
1441
1518
|
res.lastReconciledAt ? `Last usage reconciliation: ${res.lastReconciledAt}` : void 0,
|
|
1442
|
-
res.
|
|
1443
|
-
res.risks.
|
|
1444
|
-
res.ownerView ? void 0 : "Note: public view (no valid owner credential presented)."
|
|
1519
|
+
res.boundHostnames.length > 0 ? `Bound custom domains: ${res.boundHostnames.join(", ")}` : void 0,
|
|
1520
|
+
res.risks.pastDue ? "ATTENTION: renewal payment failing \u2014 update the payment method (manage_billing). Serving continues while Stripe retries; if Stripe gives up, the site reverts to free." : void 0
|
|
1445
1521
|
].filter((l) => l !== void 0);
|
|
1446
1522
|
return textJson(`${lines.join("\n")}
|
|
1447
1523
|
|
|
@@ -1451,71 +1527,18 @@ Full status:`, res);
|
|
|
1451
1527
|
}
|
|
1452
1528
|
}
|
|
1453
1529
|
);
|
|
1454
|
-
server.registerTool(
|
|
1455
|
-
"subscribe_domain",
|
|
1456
|
-
{
|
|
1457
|
-
description: `Create a Stripe Checkout link that subscribes an apex domain to a Sakupa Domain Hosting monthly plan (${planCatalog()}). Owner-only: requires this project's site credential (.sakupa/site.json) \u2014 deploy_site first; a lost credential means payment is not possible. One domain = one subscription; if the site outgrows its plan, Sakupa auto-upgrades to the next plan (capped at Business, where growth is limited instead of billed further). Paying never proves domain ownership and never activates a website \u2014 DNS verification (bind_domain) is still required. Card details are entered only on the Stripe-hosted page \u2014 never through the AI tool. Without confirm: true this tool only shows the mandatory disclosure and makes no API call.`,
|
|
1458
|
-
inputSchema: {
|
|
1459
|
-
domain: z.string().describe('Apex domain to subscribe, e.g. "example.com".'),
|
|
1460
|
-
plan: planEnum.describe(
|
|
1461
|
-
"Monthly plan: water (very light personal pages), personal (personal brand / small shop), share (small-business site), business (steadier traffic, more headroom)."
|
|
1462
|
-
),
|
|
1463
|
-
confirm: z.boolean().optional().describe("User read the subscription disclosure and confirmed. Required to proceed.")
|
|
1464
|
-
}
|
|
1465
|
-
},
|
|
1466
|
-
async (args) => {
|
|
1467
|
-
try {
|
|
1468
|
-
const site = requireSiteFile(ctx);
|
|
1469
|
-
if (args.confirm !== true) {
|
|
1470
|
-
return text(
|
|
1471
|
-
`${subscriptionWarning(args.domain, args.plan)}
|
|
1472
|
-
|
|
1473
|
-
No checkout link was created yet. Please show this disclosure to the user and, after their explicit confirmation, re-run subscribe_domain with confirm: true.`
|
|
1474
|
-
);
|
|
1475
|
-
}
|
|
1476
|
-
const res = await ctx.client.createPlanCheckout(
|
|
1477
|
-
{
|
|
1478
|
-
siteId: site.siteId,
|
|
1479
|
-
domain: args.domain,
|
|
1480
|
-
plan: args.plan,
|
|
1481
|
-
idempotencyKey: randomUUID(),
|
|
1482
|
-
confirmPlan: true
|
|
1483
|
-
},
|
|
1484
|
-
site.credential
|
|
1485
|
-
);
|
|
1486
|
-
return text(
|
|
1487
|
-
`Stripe Checkout link \u2014 Sakupa Domain Hosting for ${res.apexDomain}: ${res.plan} plan, \xA5${res.monthlyPriceJpy}/month
|
|
1488
|
-
${res.checkoutUrl}
|
|
1489
|
-
|
|
1490
|
-
Open this link in a browser to subscribe. Card data is entered only on the Stripe-hosted page \u2014 never give card numbers, passwords or security codes to the AI tool.
|
|
1491
|
-
Reminder: the subscription is payment state only. It grants no domain ownership and no management rights; the domain must still pass DNS verification (bind_domain) before long-term hosting activates.`
|
|
1492
|
-
);
|
|
1493
|
-
} catch (e) {
|
|
1494
|
-
return toolError(e);
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
);
|
|
1498
1530
|
server.registerTool(
|
|
1499
1531
|
"manage_billing",
|
|
1500
1532
|
{
|
|
1501
|
-
description: "Open the Stripe-hosted billing portal for
|
|
1502
|
-
inputSchema: {
|
|
1503
|
-
domain: z.string().optional().describe("Apex billing domain; defaults to the domain bound in .sakupa/site.json.")
|
|
1504
|
-
}
|
|
1533
|
+
description: "Open the Stripe-hosted billing portal for this site: update the payment method, view invoices, or cancel the subscription. All billing operations happen on the Stripe-hosted page \u2014 never inside the AI tool. Requires the local site credential (.sakupa/site.json).",
|
|
1534
|
+
inputSchema: {}
|
|
1505
1535
|
},
|
|
1506
|
-
async (
|
|
1536
|
+
async () => {
|
|
1507
1537
|
try {
|
|
1508
1538
|
const site = requireSiteFile(ctx);
|
|
1509
|
-
const
|
|
1510
|
-
if (!domain) {
|
|
1511
|
-
throw new SakupaError(
|
|
1512
|
-
"invalid_request",
|
|
1513
|
-
"No domain given and this project has no bound billing domain. Pass domain explicitly."
|
|
1514
|
-
);
|
|
1515
|
-
}
|
|
1516
|
-
const res = await ctx.client.createBillingPortal(domain, site.credential);
|
|
1539
|
+
const res = await ctx.client.createBillingPortal(site.siteId, site.credential);
|
|
1517
1540
|
return text(
|
|
1518
|
-
`Stripe billing portal for
|
|
1541
|
+
`Stripe billing portal for this site:
|
|
1519
1542
|
${res.portalUrl}
|
|
1520
1543
|
|
|
1521
1544
|
Open this link in a browser to update the payment method, view invoices, or manage the subscription. The link is temporary \u2014 create a fresh one when needed.`
|
|
@@ -1528,7 +1551,7 @@ Open this link in a browser to update the payment method, view invoices, or mana
|
|
|
1528
1551
|
server.registerTool(
|
|
1529
1552
|
"recover_domain_site",
|
|
1530
1553
|
{
|
|
1531
|
-
description: "Recover management control of a
|
|
1554
|
+
description: "Recover management control of a subscribed site WITH A BOUND CUSTOM DOMAIN after losing the local project, by proving DNS control of the apex domain. Sites without a bound domain are identified solely by their local credential and cannot be recovered. By default, completing recovery REVOKES all previous local credentials. Call first with the hostname to get the DNS record, then again with verificationId to complete recovery (writes a new .sakupa/site.json and returns a download link for the current site content).",
|
|
1532
1555
|
inputSchema: {
|
|
1533
1556
|
hostname: z.string().describe('Hostname of the site to recover, e.g. "www.example.com".'),
|
|
1534
1557
|
verificationId: z.string().optional().describe("Complete a recovery previously started for this hostname."),
|
|
@@ -1540,7 +1563,7 @@ Open this link in a browser to update the payment method, view invoices, or mana
|
|
|
1540
1563
|
if (args.verificationId === void 0) {
|
|
1541
1564
|
const res2 = await ctx.client.recoverDomain({ hostname: args.hostname });
|
|
1542
1565
|
return text(
|
|
1543
|
-
`Recovery started for ${args.hostname} (apex
|
|
1566
|
+
`Recovery started for ${args.hostname} (apex domain: ${res2.apexDomain}).
|
|
1544
1567
|
|
|
1545
1568
|
Create this DNS record to prove apex-domain control:
|
|
1546
1569
|
name: ${res2.verificationRecord.name}
|
|
@@ -1559,13 +1582,13 @@ After the DNS record resolves, re-run recover_domain_site with verificationId: "
|
|
|
1559
1582
|
});
|
|
1560
1583
|
writeSiteFile(ctx.projectDir, {
|
|
1561
1584
|
siteId: res.siteId,
|
|
1562
|
-
|
|
1585
|
+
...res.boundHostnames[0] !== void 0 ? { boundDomain: res.boundHostnames[0] } : {},
|
|
1563
1586
|
credential: res.credential,
|
|
1564
1587
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1565
1588
|
apiBaseUrl: ctx.apiBaseUrl
|
|
1566
1589
|
});
|
|
1567
1590
|
return text(
|
|
1568
|
-
`Recovery complete
|
|
1591
|
+
`Recovery complete.
|
|
1569
1592
|
Site: ${res.siteId} (hostnames: ${res.boundHostnames.join(", ") || "(none)"})
|
|
1570
1593
|
Previous credentials revoked: ${res.revokedPreviousCredentials ? "YES" : "no (preserved on request)"}
|
|
1571
1594
|
|
|
@@ -1582,39 +1605,32 @@ ${res.archiveUrl}`
|
|
|
1582
1605
|
server.registerTool(
|
|
1583
1606
|
"set_billing_plan",
|
|
1584
1607
|
{
|
|
1585
|
-
description: `Change
|
|
1608
|
+
description: `Change this site's hosting plan, cancel/re-enable renewal, or cancel immediately. Plans: ${planCatalog()}. Plan changes go through the Stripe subscription; renewals bill the new plan. Auto-upgrade (one plan up when usage exceeds the current plan, capped at Business) is built in and not configurable. cancelRenewal: true ends the subscription at the period end; cancelNow: true ends it IMMEDIATELY \u2014 the site reverts to a free 24h site at once and all paid data (custom domain bindings included) is removed, with no refund of the current period. The API returns the consequences first; explicit owner confirmation (confirm: true) is required before anything is applied.`,
|
|
1586
1609
|
inputSchema: {
|
|
1587
|
-
domain: z.string().optional().describe("Apex billing domain; defaults to the domain bound in .sakupa/site.json."),
|
|
1588
1610
|
plan: planEnum.optional().describe("Target monthly plan."),
|
|
1589
1611
|
cancelRenewal: z.boolean().optional().describe(
|
|
1590
|
-
"true: cancel renewal (
|
|
1612
|
+
"true: cancel renewal (the site stays permanent to the end of the paid month, then reverts to free). false: re-enable renewal."
|
|
1591
1613
|
),
|
|
1614
|
+
cancelNow: z.boolean().optional().describe("End the subscription immediately; the site reverts to free right away."),
|
|
1592
1615
|
confirm: z.boolean().optional().describe("User saw the consequences and explicitly confirmed.")
|
|
1593
1616
|
}
|
|
1594
1617
|
},
|
|
1595
1618
|
async (args) => {
|
|
1596
1619
|
try {
|
|
1597
1620
|
const site = requireSiteFile(ctx);
|
|
1598
|
-
const domain = args.domain ?? site.billingDomain;
|
|
1599
|
-
if (!domain) {
|
|
1600
|
-
throw new SakupaError(
|
|
1601
|
-
"invalid_request",
|
|
1602
|
-
"No domain given and this project has no bound billing domain. Pass domain explicitly."
|
|
1603
|
-
);
|
|
1604
|
-
}
|
|
1605
1621
|
const req = {
|
|
1606
|
-
|
|
1607
|
-
...args.plan !== void 0 ? { committedTier: args.plan } : {},
|
|
1622
|
+
...args.plan !== void 0 ? { plan: args.plan } : {},
|
|
1608
1623
|
...args.cancelRenewal !== void 0 ? { cancelRenewal: args.cancelRenewal } : {},
|
|
1624
|
+
...args.cancelNow !== void 0 ? { cancelNow: args.cancelNow } : {},
|
|
1609
1625
|
confirm: args.confirm === true
|
|
1610
1626
|
};
|
|
1611
1627
|
try {
|
|
1612
|
-
const res = await ctx.client.setBillingPlan(
|
|
1628
|
+
const res = await ctx.client.setBillingPlan(site.siteId, site.credential, req);
|
|
1613
1629
|
return textJson(
|
|
1614
|
-
`Billing
|
|
1630
|
+
`Billing updated for site ${res.siteId} (mode: ${res.mode}).
|
|
1615
1631
|
Consequences:
|
|
1616
1632
|
${res.consequences.map((c) => `- ${c}`).join("\n")}
|
|
1617
|
-
|
|
1633
|
+
Result:`,
|
|
1618
1634
|
res
|
|
1619
1635
|
);
|
|
1620
1636
|
} catch (e) {
|
|
@@ -1634,50 +1650,6 @@ ${e.message}
|
|
|
1634
1650
|
}
|
|
1635
1651
|
}
|
|
1636
1652
|
);
|
|
1637
|
-
server.registerTool(
|
|
1638
|
-
"downgrade_to_free",
|
|
1639
|
-
{
|
|
1640
|
-
description: "Move this paid custom-domain site back to free temporary mode. Takes effect at the next billing period boundary by default; immediateStopServing stops custom-domain serving now but never erases the already-paid month. Remember to also cancel the domain subscription (set_billing_plan cancelRenewal: true or manage_billing) if no other site uses it. The API returns the consequences first; confirm: true is required to apply.",
|
|
1641
|
-
inputSchema: {
|
|
1642
|
-
immediateStopServing: z.boolean().optional().describe(
|
|
1643
|
-
"Also stop custom-domain serving immediately (does not remove current-cycle charges)."
|
|
1644
|
-
),
|
|
1645
|
-
confirm: z.boolean().optional().describe("User saw the consequences and explicitly confirmed.")
|
|
1646
|
-
}
|
|
1647
|
-
},
|
|
1648
|
-
async (args) => {
|
|
1649
|
-
try {
|
|
1650
|
-
const site = requireSiteFile(ctx);
|
|
1651
|
-
try {
|
|
1652
|
-
const res = await ctx.client.downgradeSite(site.siteId, site.credential, {
|
|
1653
|
-
...args.immediateStopServing !== void 0 ? { immediateStopServing: args.immediateStopServing } : {},
|
|
1654
|
-
confirm: args.confirm === true
|
|
1655
|
-
});
|
|
1656
|
-
return text(
|
|
1657
|
-
`Downgrade scheduled for site ${res.siteId}.
|
|
1658
|
-
Effective at: ${res.effectiveAt}
|
|
1659
|
-
Immediate stop-serving applied: ${res.immediateStopApplied ? "yes" : "no"}
|
|
1660
|
-
${res.message}
|
|
1661
|
-
|
|
1662
|
-
After it takes effect the site becomes a free temporary site again (24h validity, free banner, free limits). If no other site uses this domain, also cancel its hosting subscription (set_billing_plan cancelRenewal: true or manage_billing).`
|
|
1663
|
-
);
|
|
1664
|
-
} catch (e) {
|
|
1665
|
-
if (isSakupaError(e) && e.code === "confirmation_required") {
|
|
1666
|
-
return text(
|
|
1667
|
-
`Confirmation required before downgrading \u2014 nothing was applied.
|
|
1668
|
-
|
|
1669
|
-
${e.message}
|
|
1670
|
-
` + (e.details !== void 0 ? `${JSON.stringify(e.details, null, 2)}
|
|
1671
|
-
` : "") + "\nPlease show these consequences to the user and, after their explicit confirmation, re-run downgrade_to_free with confirm: true."
|
|
1672
|
-
);
|
|
1673
|
-
}
|
|
1674
|
-
throw e;
|
|
1675
|
-
}
|
|
1676
|
-
} catch (e) {
|
|
1677
|
-
return toolError(e);
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
);
|
|
1681
1653
|
server.registerTool(
|
|
1682
1654
|
"create_support_ticket",
|
|
1683
1655
|
{
|
|
@@ -1708,7 +1680,7 @@ ${e.message}
|
|
|
1708
1680
|
server.registerTool(
|
|
1709
1681
|
"report_bug",
|
|
1710
1682
|
{
|
|
1711
|
-
description: "Prepare and submit a sanitized bug report when a Sakupa tool failed and the issue looks like a product bug. Only whitelisted structured diagnostics are sent (tool name, error code/message, site id,
|
|
1683
|
+
description: "Prepare and submit a sanitized bug report when a Sakupa tool failed and the issue looks like a product bug. Only whitelisted structured diagnostics are sent (tool name, error code/message, site id, bound domain, deployment id, timestamps, client/MCP version, request id) \u2014 NEVER file contents, source code, secrets, .env values or credentials. Without confirmSubmit: true the exact payload is shown for user review and nothing is submitted.",
|
|
1712
1684
|
inputSchema: {
|
|
1713
1685
|
toolName: z.string().describe('The Sakupa tool that failed, e.g. "deploy_site".'),
|
|
1714
1686
|
errorCode: z.string().optional(),
|
|
@@ -1728,7 +1700,7 @@ ${e.message}
|
|
|
1728
1700
|
...args.errorCode !== void 0 ? { errorCode: args.errorCode } : {},
|
|
1729
1701
|
...args.errorMessage !== void 0 ? { sanitizedErrorMessage: args.errorMessage } : {},
|
|
1730
1702
|
...site ? { siteId: site.siteId } : {},
|
|
1731
|
-
...site?.
|
|
1703
|
+
...site?.boundDomain ? { boundDomain: site.boundDomain } : {},
|
|
1732
1704
|
...args.deploymentId !== void 0 ? { deploymentId: args.deploymentId } : {},
|
|
1733
1705
|
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1734
1706
|
clientType: CLIENT_TYPE,
|
|
@@ -1772,6 +1744,7 @@ var FetchTransport = class {
|
|
|
1772
1744
|
}
|
|
1773
1745
|
const headers = {
|
|
1774
1746
|
accept: "application/json",
|
|
1747
|
+
[MCP_VERSION_HEADER]: MCP_VERSION,
|
|
1775
1748
|
...req.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
1776
1749
|
...req.headers
|
|
1777
1750
|
};
|
|
@@ -1822,19 +1795,23 @@ Workflow:
|
|
|
1822
1795
|
site (public URL {shortId}.sakupa.com, valid 24 hours, free banner shown) and stores the
|
|
1823
1796
|
management credential in .sakupa/site.json. Deploying again updates the site and refreshes
|
|
1824
1797
|
its validity; refresh_site extends validity without uploading.
|
|
1825
|
-
3.
|
|
1798
|
+
3. To make the site PERMANENT, subscribe it to a monthly hosting plan (subscribe_site ->
|
|
1826
1799
|
Stripe-hosted checkout; water/personal/share/business, auto-upgrade when the site outgrows
|
|
1827
|
-
its plan)
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1800
|
+
its plan). Paying makes the {shortId}.sakupa.com URL permanent \u2014 that is what payment buys.
|
|
1801
|
+
4. Optionally bind a custom domain to the subscribed site (bind_domain): an included extra
|
|
1802
|
+
serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
|
|
1803
|
+
first verified request wins; unverified requests expire after 72 hours. billing_status,
|
|
1804
|
+
set_billing_plan, manage_billing and recover_domain_site manage the paid lifecycle.
|
|
1805
|
+
Canceling the subscription immediately reverts the site to a free 24h site and removes all
|
|
1806
|
+
paid data.
|
|
1831
1807
|
|
|
1832
1808
|
Safety boundaries:
|
|
1833
1809
|
- Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
|
|
1834
1810
|
- Never upload source projects, secrets, .env files, private keys, archives, videos or audio.
|
|
1835
1811
|
- Payment card data is entered only on Stripe-hosted pages \u2014 never through the AI tool.
|
|
1836
|
-
- A subscription
|
|
1837
|
-
- The management credential lives only in .sakupa/site.json; never share or upload it
|
|
1812
|
+
- A subscription never grants domain ownership; only DNS verification does.
|
|
1813
|
+
- The management credential lives only in .sakupa/site.json; never share or upload it. Without
|
|
1814
|
+
a bound custom domain, a lost credential is unrecoverable by design.`;
|
|
1838
1815
|
function createSakupaMcpServer(opts) {
|
|
1839
1816
|
const client = opts.client ?? new HttpApiClient(new FetchTransport(opts.apiBaseUrl));
|
|
1840
1817
|
const server = new McpServer(
|