@sakupa/mcp 0.2.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 +113 -199
- package/dist/index.js +165 -251
- 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 {
|
|
@@ -450,11 +450,12 @@ async function sha256Hex(bytes) {
|
|
|
450
450
|
}
|
|
451
451
|
|
|
452
452
|
// ../core/dist/domain/subscription.js
|
|
453
|
-
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.";
|
|
454
454
|
|
|
455
455
|
// ../core/dist/dto.js
|
|
456
456
|
var CREDENTIAL_HEADER = "x-sakupa-credential";
|
|
457
457
|
var IDEMPOTENCY_HEADER = "x-sakupa-idempotency-key";
|
|
458
|
+
var MCP_VERSION_HEADER = "x-sakupa-mcp-version";
|
|
458
459
|
|
|
459
460
|
// ../core/dist/services/sites.js
|
|
460
461
|
var utf8Decoder = new TextDecoder("utf-8", { fatal: false });
|
|
@@ -473,11 +474,11 @@ var KNOWN_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
473
474
|
"conflict",
|
|
474
475
|
"state_conflict",
|
|
475
476
|
"rate_limited",
|
|
476
|
-
"insufficient_balance",
|
|
477
477
|
"payment_required",
|
|
478
478
|
"confirmation_required",
|
|
479
479
|
"dns_not_verified",
|
|
480
480
|
"not_deployable",
|
|
481
|
+
"upgrade_required",
|
|
481
482
|
"internal"
|
|
482
483
|
]);
|
|
483
484
|
var HttpApiClient = class {
|
|
@@ -578,14 +579,7 @@ var HttpApiClient = class {
|
|
|
578
579
|
{ body: req }
|
|
579
580
|
);
|
|
580
581
|
}
|
|
581
|
-
/**
|
|
582
|
-
async createCheckout(req) {
|
|
583
|
-
return this.call("POST", "/v1/payments/checkout", {
|
|
584
|
-
body: req,
|
|
585
|
-
idempotencyKey: req.idempotencyKey
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
/** Subscription checkout: a fixed monthly plan for one apex domain (owner-only). */
|
|
582
|
+
/** Subscription checkout: a fixed monthly plan permanentizing one site (owner-only). */
|
|
589
583
|
async createPlanCheckout(req, credential) {
|
|
590
584
|
return this.call("POST", "/v1/payments/checkout", {
|
|
591
585
|
credential,
|
|
@@ -594,31 +588,24 @@ var HttpApiClient = class {
|
|
|
594
588
|
});
|
|
595
589
|
}
|
|
596
590
|
/** Stripe-hosted billing portal (payment method, invoices, cancellation). */
|
|
597
|
-
async createBillingPortal(
|
|
591
|
+
async createBillingPortal(siteId, credential) {
|
|
598
592
|
return this.call(
|
|
599
593
|
"POST",
|
|
600
|
-
`/v1/
|
|
594
|
+
`/v1/sites/${encodeURIComponent(siteId)}/billing/portal`,
|
|
601
595
|
{ credential, body: {} }
|
|
602
596
|
);
|
|
603
597
|
}
|
|
604
|
-
async getBillingStatus(
|
|
598
|
+
async getBillingStatus(siteId, credential) {
|
|
605
599
|
return this.call(
|
|
606
600
|
"GET",
|
|
607
|
-
`/v1/
|
|
608
|
-
|
|
609
|
-
);
|
|
610
|
-
}
|
|
611
|
-
async setBillingPlan(apexDomain, credential, req) {
|
|
612
|
-
return this.call(
|
|
613
|
-
"POST",
|
|
614
|
-
`/v1/billing/${encodeURIComponent(apexDomain)}/plan`,
|
|
615
|
-
{ credential, body: req }
|
|
601
|
+
`/v1/sites/${encodeURIComponent(siteId)}/billing`,
|
|
602
|
+
{ credential }
|
|
616
603
|
);
|
|
617
604
|
}
|
|
618
|
-
async
|
|
605
|
+
async setBillingPlan(siteId, credential, req) {
|
|
619
606
|
return this.call(
|
|
620
607
|
"POST",
|
|
621
|
-
`/v1/sites/${encodeURIComponent(siteId)}/
|
|
608
|
+
`/v1/sites/${encodeURIComponent(siteId)}/billing/plan`,
|
|
622
609
|
{ credential, body: req }
|
|
623
610
|
);
|
|
624
611
|
}
|
|
@@ -1096,8 +1083,7 @@ function readSiteFile(projectDir) {
|
|
|
1096
1083
|
apiBaseUrl: typeof parsed.apiBaseUrl === "string" ? parsed.apiBaseUrl : "",
|
|
1097
1084
|
...typeof parsed.shortId === "string" ? { shortId: parsed.shortId } : {},
|
|
1098
1085
|
...typeof parsed.url === "string" ? { url: parsed.url } : {},
|
|
1099
|
-
...typeof parsed.
|
|
1100
|
-
...typeof parsed.billingDomainId === "string" ? { billingDomainId: parsed.billingDomainId } : {}
|
|
1086
|
+
...typeof parsed.boundDomain === "string" ? { boundDomain: parsed.boundDomain } : {}
|
|
1101
1087
|
};
|
|
1102
1088
|
} catch {
|
|
1103
1089
|
return null;
|
|
@@ -1116,7 +1102,7 @@ function writeSiteFile(projectDir, file) {
|
|
|
1116
1102
|
}
|
|
1117
1103
|
|
|
1118
1104
|
// src/version.ts
|
|
1119
|
-
var MCP_VERSION = "0.
|
|
1105
|
+
var MCP_VERSION = "0.3.0";
|
|
1120
1106
|
var CLIENT_TYPE = "sakupa-mcp";
|
|
1121
1107
|
|
|
1122
1108
|
// src/tools/context.ts
|
|
@@ -1156,8 +1142,8 @@ var severityEnum = z.enum(["low", "medium", "high", "critical"]);
|
|
|
1156
1142
|
function planCatalog() {
|
|
1157
1143
|
return TIER_ORDER.map((p) => `${p} \xA5${tierPriceJpy(p)}/month`).join(", ");
|
|
1158
1144
|
}
|
|
1159
|
-
function subscriptionWarning(
|
|
1160
|
-
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)));
|
|
1161
1147
|
}
|
|
1162
1148
|
var ticketCategoryEnum = z.enum([
|
|
1163
1149
|
"billing",
|
|
@@ -1210,7 +1196,7 @@ function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
|
|
|
1210
1196
|
if (total > FREE_SITE_MAX_TOTAL_BYTES) {
|
|
1211
1197
|
throw new SakupaError(
|
|
1212
1198
|
"validation_failed",
|
|
1213
|
-
`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
|
|
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.`
|
|
1214
1200
|
);
|
|
1215
1201
|
}
|
|
1216
1202
|
}
|
|
@@ -1279,7 +1265,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1279
1265
|
server.registerTool(
|
|
1280
1266
|
"deploy_site",
|
|
1281
1267
|
{
|
|
1282
|
-
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.`,
|
|
1283
1269
|
inputSchema: {
|
|
1284
1270
|
outputDir: z.string().optional().describe("Output directory relative to the project root (overrides detection)."),
|
|
1285
1271
|
spaFallback: z.boolean().optional().describe("Enable SPA fallback (requires spaFallbackConfirmed: true)."),
|
|
@@ -1331,7 +1317,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1331
1317
|
Files uploaded: ${uploaded2} (${finalized2.totalBytes} bytes)
|
|
1332
1318
|
` + (finalized2.expiresAt ? `Expires at: ${finalized2.expiresAt}
|
|
1333
1319
|
` : "") + `
|
|
1334
|
-
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.
|
|
1335
1321
|
` + (finalized2.warnings.length > 0 ? `
|
|
1336
1322
|
Warnings:
|
|
1337
1323
|
${JSON.stringify(finalized2.warnings, null, 2)}` : "")
|
|
@@ -1372,8 +1358,8 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : "")
|
|
|
1372
1358
|
Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
|
|
1373
1359
|
` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
|
|
1374
1360
|
` : "") + (finalized.mode === "free" ? `
|
|
1375
|
-
Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh_site call.
|
|
1376
|
-
` : "") + (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 ? `
|
|
1377
1363
|
Warnings:
|
|
1378
1364
|
${JSON.stringify(finalized.warnings, null, 2)}` : "")
|
|
1379
1365
|
);
|
|
@@ -1385,7 +1371,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : "")
|
|
|
1385
1371
|
server.registerTool(
|
|
1386
1372
|
"refresh_site",
|
|
1387
1373
|
{
|
|
1388
|
-
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.",
|
|
1389
1375
|
inputSchema: {}
|
|
1390
1376
|
},
|
|
1391
1377
|
async () => {
|
|
@@ -1404,7 +1390,7 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1404
1390
|
server.registerTool(
|
|
1405
1391
|
"site_status",
|
|
1406
1392
|
{
|
|
1407
|
-
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.",
|
|
1408
1394
|
inputSchema: {}
|
|
1409
1395
|
},
|
|
1410
1396
|
async () => {
|
|
@@ -1417,10 +1403,53 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1417
1403
|
}
|
|
1418
1404
|
}
|
|
1419
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
|
+
);
|
|
1420
1449
|
server.registerTool(
|
|
1421
1450
|
"bind_domain",
|
|
1422
1451
|
{
|
|
1423
|
-
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.",
|
|
1424
1453
|
inputSchema: {
|
|
1425
1454
|
hostname: z.string().describe('Hostname to bind, e.g. "example.com" or "www.example.com".'),
|
|
1426
1455
|
verificationId: z.string().optional().describe("Check an existing DNS verification instead of starting a new one.")
|
|
@@ -1432,12 +1461,12 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1432
1461
|
if (args.verificationId !== void 0) {
|
|
1433
1462
|
const res2 = await ctx.client.checkVerification(args.verificationId, site.credential);
|
|
1434
1463
|
if (res2.status === "verified") {
|
|
1435
|
-
writeSiteFile(ctx.projectDir, { ...site,
|
|
1464
|
+
writeSiteFile(ctx.projectDir, { ...site, boundDomain: res2.apexDomain });
|
|
1436
1465
|
}
|
|
1437
1466
|
return text(
|
|
1438
1467
|
`DNS verification ${res2.verificationId}: ${res2.status}
|
|
1439
1468
|
${res2.message}
|
|
1440
|
-
` + (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.
|
|
1441
1470
|
` : "") + (res2.pendingDnsRecords.length > 0 ? `
|
|
1442
1471
|
DNS records still required:
|
|
1443
1472
|
${JSON.stringify(res2.pendingDnsRecords, null, 2)}` : "")
|
|
@@ -1448,22 +1477,15 @@ ${JSON.stringify(res2.pendingDnsRecords, null, 2)}` : "")
|
|
|
1448
1477
|
hostname: args.hostname
|
|
1449
1478
|
};
|
|
1450
1479
|
const res = await ctx.client.bindDomain(site.credential, req);
|
|
1451
|
-
writeSiteFile(ctx.projectDir, {
|
|
1452
|
-
...site,
|
|
1453
|
-
billingDomain: res.apexDomain,
|
|
1454
|
-
billingDomainId: res.billingDomainId
|
|
1455
|
-
});
|
|
1456
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.`;
|
|
1457
1481
|
return text(
|
|
1458
1482
|
`Domain binding started for ${res.hostname}.
|
|
1459
1483
|
|
|
1460
|
-
Billing subject: the apex domain "${res.apexDomain}". Subdomains of ${res.apexDomain} cannot be separate billing subjects \u2014 they all share this domain hosting subscription.
|
|
1461
|
-
|
|
1462
1484
|
1. Prove control of ${res.apexDomain} by creating this DNS record:
|
|
1463
1485
|
name: ${res.verificationRecord.name}
|
|
1464
1486
|
type: ${res.verificationRecord.type}
|
|
1465
1487
|
value: ${res.verificationRecord.value}
|
|
1466
|
-
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.
|
|
1467
1489
|
|
|
1468
1490
|
2. Serving DNS (after verification): ${servingHint}
|
|
1469
1491
|
Server guidance: ${res.servingInstructions}
|
|
@@ -1478,33 +1500,24 @@ Then run bind_domain again with verificationId: "${res.verificationId}" to check
|
|
|
1478
1500
|
server.registerTool(
|
|
1479
1501
|
"billing_status",
|
|
1480
1502
|
{
|
|
1481
|
-
description: "Show
|
|
1482
|
-
inputSchema: {
|
|
1483
|
-
domain: z.string().optional().describe("Apex billing domain; defaults to the domain bound in .sakupa/site.json.")
|
|
1484
|
-
}
|
|
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: {}
|
|
1485
1505
|
},
|
|
1486
|
-
async (
|
|
1506
|
+
async () => {
|
|
1487
1507
|
try {
|
|
1488
|
-
const site =
|
|
1489
|
-
const
|
|
1490
|
-
if (!domain) {
|
|
1491
|
-
throw new SakupaError(
|
|
1492
|
-
"invalid_request",
|
|
1493
|
-
'No domain given and this project has no bound billing domain. Pass domain, e.g. billing_status { domain: "example.com" }.'
|
|
1494
|
-
);
|
|
1495
|
-
}
|
|
1496
|
-
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);
|
|
1497
1510
|
const lines = [
|
|
1498
|
-
`Billing status for ${res.
|
|
1499
|
-
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)",
|
|
1500
1514
|
res.subscriptionStatus ? `Subscription payment state: ${res.subscriptionStatus}` : void 0,
|
|
1501
|
-
res.
|
|
1502
|
-
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,
|
|
1503
1517
|
res.estimatedUsageTier ? `Estimated usage tier: ${res.estimatedUsageTier}` : void 0,
|
|
1504
1518
|
res.lastReconciledAt ? `Last usage reconciliation: ${res.lastReconciledAt}` : void 0,
|
|
1505
|
-
res.
|
|
1506
|
-
res.risks.
|
|
1507
|
-
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
|
|
1508
1521
|
].filter((l) => l !== void 0);
|
|
1509
1522
|
return textJson(`${lines.join("\n")}
|
|
1510
1523
|
|
|
@@ -1514,71 +1527,18 @@ Full status:`, res);
|
|
|
1514
1527
|
}
|
|
1515
1528
|
}
|
|
1516
1529
|
);
|
|
1517
|
-
server.registerTool(
|
|
1518
|
-
"subscribe_domain",
|
|
1519
|
-
{
|
|
1520
|
-
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.`,
|
|
1521
|
-
inputSchema: {
|
|
1522
|
-
domain: z.string().describe('Apex domain to subscribe, e.g. "example.com".'),
|
|
1523
|
-
plan: planEnum.describe(
|
|
1524
|
-
"Monthly plan: water (very light personal pages), personal (personal brand / small shop), share (small-business site), business (steadier traffic, more headroom)."
|
|
1525
|
-
),
|
|
1526
|
-
confirm: z.boolean().optional().describe("User read the subscription disclosure and confirmed. Required to proceed.")
|
|
1527
|
-
}
|
|
1528
|
-
},
|
|
1529
|
-
async (args) => {
|
|
1530
|
-
try {
|
|
1531
|
-
const site = requireSiteFile(ctx);
|
|
1532
|
-
if (args.confirm !== true) {
|
|
1533
|
-
return text(
|
|
1534
|
-
`${subscriptionWarning(args.domain, args.plan)}
|
|
1535
|
-
|
|
1536
|
-
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.`
|
|
1537
|
-
);
|
|
1538
|
-
}
|
|
1539
|
-
const res = await ctx.client.createPlanCheckout(
|
|
1540
|
-
{
|
|
1541
|
-
siteId: site.siteId,
|
|
1542
|
-
domain: args.domain,
|
|
1543
|
-
plan: args.plan,
|
|
1544
|
-
idempotencyKey: randomUUID(),
|
|
1545
|
-
confirmPlan: true
|
|
1546
|
-
},
|
|
1547
|
-
site.credential
|
|
1548
|
-
);
|
|
1549
|
-
return text(
|
|
1550
|
-
`Stripe Checkout link \u2014 Sakupa Domain Hosting for ${res.apexDomain}: ${res.plan} plan, \xA5${res.monthlyPriceJpy}/month
|
|
1551
|
-
${res.checkoutUrl}
|
|
1552
|
-
|
|
1553
|
-
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.
|
|
1554
|
-
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.`
|
|
1555
|
-
);
|
|
1556
|
-
} catch (e) {
|
|
1557
|
-
return toolError(e);
|
|
1558
|
-
}
|
|
1559
|
-
}
|
|
1560
|
-
);
|
|
1561
1530
|
server.registerTool(
|
|
1562
1531
|
"manage_billing",
|
|
1563
1532
|
{
|
|
1564
|
-
description: "Open the Stripe-hosted billing portal for
|
|
1565
|
-
inputSchema: {
|
|
1566
|
-
domain: z.string().optional().describe("Apex billing domain; defaults to the domain bound in .sakupa/site.json.")
|
|
1567
|
-
}
|
|
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: {}
|
|
1568
1535
|
},
|
|
1569
|
-
async (
|
|
1536
|
+
async () => {
|
|
1570
1537
|
try {
|
|
1571
1538
|
const site = requireSiteFile(ctx);
|
|
1572
|
-
const
|
|
1573
|
-
if (!domain) {
|
|
1574
|
-
throw new SakupaError(
|
|
1575
|
-
"invalid_request",
|
|
1576
|
-
"No domain given and this project has no bound billing domain. Pass domain explicitly."
|
|
1577
|
-
);
|
|
1578
|
-
}
|
|
1579
|
-
const res = await ctx.client.createBillingPortal(domain, site.credential);
|
|
1539
|
+
const res = await ctx.client.createBillingPortal(site.siteId, site.credential);
|
|
1580
1540
|
return text(
|
|
1581
|
-
`Stripe billing portal for
|
|
1541
|
+
`Stripe billing portal for this site:
|
|
1582
1542
|
${res.portalUrl}
|
|
1583
1543
|
|
|
1584
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.`
|
|
@@ -1591,7 +1551,7 @@ Open this link in a browser to update the payment method, view invoices, or mana
|
|
|
1591
1551
|
server.registerTool(
|
|
1592
1552
|
"recover_domain_site",
|
|
1593
1553
|
{
|
|
1594
|
-
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).",
|
|
1595
1555
|
inputSchema: {
|
|
1596
1556
|
hostname: z.string().describe('Hostname of the site to recover, e.g. "www.example.com".'),
|
|
1597
1557
|
verificationId: z.string().optional().describe("Complete a recovery previously started for this hostname."),
|
|
@@ -1603,7 +1563,7 @@ Open this link in a browser to update the payment method, view invoices, or mana
|
|
|
1603
1563
|
if (args.verificationId === void 0) {
|
|
1604
1564
|
const res2 = await ctx.client.recoverDomain({ hostname: args.hostname });
|
|
1605
1565
|
return text(
|
|
1606
|
-
`Recovery started for ${args.hostname} (apex
|
|
1566
|
+
`Recovery started for ${args.hostname} (apex domain: ${res2.apexDomain}).
|
|
1607
1567
|
|
|
1608
1568
|
Create this DNS record to prove apex-domain control:
|
|
1609
1569
|
name: ${res2.verificationRecord.name}
|
|
@@ -1622,13 +1582,13 @@ After the DNS record resolves, re-run recover_domain_site with verificationId: "
|
|
|
1622
1582
|
});
|
|
1623
1583
|
writeSiteFile(ctx.projectDir, {
|
|
1624
1584
|
siteId: res.siteId,
|
|
1625
|
-
|
|
1585
|
+
...res.boundHostnames[0] !== void 0 ? { boundDomain: res.boundHostnames[0] } : {},
|
|
1626
1586
|
credential: res.credential,
|
|
1627
1587
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1628
1588
|
apiBaseUrl: ctx.apiBaseUrl
|
|
1629
1589
|
});
|
|
1630
1590
|
return text(
|
|
1631
|
-
`Recovery complete
|
|
1591
|
+
`Recovery complete.
|
|
1632
1592
|
Site: ${res.siteId} (hostnames: ${res.boundHostnames.join(", ") || "(none)"})
|
|
1633
1593
|
Previous credentials revoked: ${res.revokedPreviousCredentials ? "YES" : "no (preserved on request)"}
|
|
1634
1594
|
|
|
@@ -1645,39 +1605,32 @@ ${res.archiveUrl}`
|
|
|
1645
1605
|
server.registerTool(
|
|
1646
1606
|
"set_billing_plan",
|
|
1647
1607
|
{
|
|
1648
|
-
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.`,
|
|
1649
1609
|
inputSchema: {
|
|
1650
|
-
domain: z.string().optional().describe("Apex billing domain; defaults to the domain bound in .sakupa/site.json."),
|
|
1651
1610
|
plan: planEnum.optional().describe("Target monthly plan."),
|
|
1652
1611
|
cancelRenewal: z.boolean().optional().describe(
|
|
1653
|
-
"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."
|
|
1654
1613
|
),
|
|
1614
|
+
cancelNow: z.boolean().optional().describe("End the subscription immediately; the site reverts to free right away."),
|
|
1655
1615
|
confirm: z.boolean().optional().describe("User saw the consequences and explicitly confirmed.")
|
|
1656
1616
|
}
|
|
1657
1617
|
},
|
|
1658
1618
|
async (args) => {
|
|
1659
1619
|
try {
|
|
1660
1620
|
const site = requireSiteFile(ctx);
|
|
1661
|
-
const domain = args.domain ?? site.billingDomain;
|
|
1662
|
-
if (!domain) {
|
|
1663
|
-
throw new SakupaError(
|
|
1664
|
-
"invalid_request",
|
|
1665
|
-
"No domain given and this project has no bound billing domain. Pass domain explicitly."
|
|
1666
|
-
);
|
|
1667
|
-
}
|
|
1668
1621
|
const req = {
|
|
1669
|
-
|
|
1670
|
-
...args.plan !== void 0 ? { committedTier: args.plan } : {},
|
|
1622
|
+
...args.plan !== void 0 ? { plan: args.plan } : {},
|
|
1671
1623
|
...args.cancelRenewal !== void 0 ? { cancelRenewal: args.cancelRenewal } : {},
|
|
1624
|
+
...args.cancelNow !== void 0 ? { cancelNow: args.cancelNow } : {},
|
|
1672
1625
|
confirm: args.confirm === true
|
|
1673
1626
|
};
|
|
1674
1627
|
try {
|
|
1675
|
-
const res = await ctx.client.setBillingPlan(
|
|
1628
|
+
const res = await ctx.client.setBillingPlan(site.siteId, site.credential, req);
|
|
1676
1629
|
return textJson(
|
|
1677
|
-
`Billing
|
|
1630
|
+
`Billing updated for site ${res.siteId} (mode: ${res.mode}).
|
|
1678
1631
|
Consequences:
|
|
1679
1632
|
${res.consequences.map((c) => `- ${c}`).join("\n")}
|
|
1680
|
-
|
|
1633
|
+
Result:`,
|
|
1681
1634
|
res
|
|
1682
1635
|
);
|
|
1683
1636
|
} catch (e) {
|
|
@@ -1697,50 +1650,6 @@ ${e.message}
|
|
|
1697
1650
|
}
|
|
1698
1651
|
}
|
|
1699
1652
|
);
|
|
1700
|
-
server.registerTool(
|
|
1701
|
-
"downgrade_to_free",
|
|
1702
|
-
{
|
|
1703
|
-
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.",
|
|
1704
|
-
inputSchema: {
|
|
1705
|
-
immediateStopServing: z.boolean().optional().describe(
|
|
1706
|
-
"Also stop custom-domain serving immediately (does not remove current-cycle charges)."
|
|
1707
|
-
),
|
|
1708
|
-
confirm: z.boolean().optional().describe("User saw the consequences and explicitly confirmed.")
|
|
1709
|
-
}
|
|
1710
|
-
},
|
|
1711
|
-
async (args) => {
|
|
1712
|
-
try {
|
|
1713
|
-
const site = requireSiteFile(ctx);
|
|
1714
|
-
try {
|
|
1715
|
-
const res = await ctx.client.downgradeSite(site.siteId, site.credential, {
|
|
1716
|
-
...args.immediateStopServing !== void 0 ? { immediateStopServing: args.immediateStopServing } : {},
|
|
1717
|
-
confirm: args.confirm === true
|
|
1718
|
-
});
|
|
1719
|
-
return text(
|
|
1720
|
-
`Downgrade scheduled for site ${res.siteId}.
|
|
1721
|
-
Effective at: ${res.effectiveAt}
|
|
1722
|
-
Immediate stop-serving applied: ${res.immediateStopApplied ? "yes" : "no"}
|
|
1723
|
-
${res.message}
|
|
1724
|
-
|
|
1725
|
-
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).`
|
|
1726
|
-
);
|
|
1727
|
-
} catch (e) {
|
|
1728
|
-
if (isSakupaError(e) && e.code === "confirmation_required") {
|
|
1729
|
-
return text(
|
|
1730
|
-
`Confirmation required before downgrading \u2014 nothing was applied.
|
|
1731
|
-
|
|
1732
|
-
${e.message}
|
|
1733
|
-
` + (e.details !== void 0 ? `${JSON.stringify(e.details, null, 2)}
|
|
1734
|
-
` : "") + "\nPlease show these consequences to the user and, after their explicit confirmation, re-run downgrade_to_free with confirm: true."
|
|
1735
|
-
);
|
|
1736
|
-
}
|
|
1737
|
-
throw e;
|
|
1738
|
-
}
|
|
1739
|
-
} catch (e) {
|
|
1740
|
-
return toolError(e);
|
|
1741
|
-
}
|
|
1742
|
-
}
|
|
1743
|
-
);
|
|
1744
1653
|
server.registerTool(
|
|
1745
1654
|
"create_support_ticket",
|
|
1746
1655
|
{
|
|
@@ -1771,7 +1680,7 @@ ${e.message}
|
|
|
1771
1680
|
server.registerTool(
|
|
1772
1681
|
"report_bug",
|
|
1773
1682
|
{
|
|
1774
|
-
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.",
|
|
1775
1684
|
inputSchema: {
|
|
1776
1685
|
toolName: z.string().describe('The Sakupa tool that failed, e.g. "deploy_site".'),
|
|
1777
1686
|
errorCode: z.string().optional(),
|
|
@@ -1791,7 +1700,7 @@ ${e.message}
|
|
|
1791
1700
|
...args.errorCode !== void 0 ? { errorCode: args.errorCode } : {},
|
|
1792
1701
|
...args.errorMessage !== void 0 ? { sanitizedErrorMessage: args.errorMessage } : {},
|
|
1793
1702
|
...site ? { siteId: site.siteId } : {},
|
|
1794
|
-
...site?.
|
|
1703
|
+
...site?.boundDomain ? { boundDomain: site.boundDomain } : {},
|
|
1795
1704
|
...args.deploymentId !== void 0 ? { deploymentId: args.deploymentId } : {},
|
|
1796
1705
|
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1797
1706
|
clientType: CLIENT_TYPE,
|
|
@@ -1835,6 +1744,7 @@ var FetchTransport = class {
|
|
|
1835
1744
|
}
|
|
1836
1745
|
const headers = {
|
|
1837
1746
|
accept: "application/json",
|
|
1747
|
+
[MCP_VERSION_HEADER]: MCP_VERSION,
|
|
1838
1748
|
...req.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
1839
1749
|
...req.headers
|
|
1840
1750
|
};
|
|
@@ -1885,19 +1795,23 @@ Workflow:
|
|
|
1885
1795
|
site (public URL {shortId}.sakupa.com, valid 24 hours, free banner shown) and stores the
|
|
1886
1796
|
management credential in .sakupa/site.json. Deploying again updates the site and refreshes
|
|
1887
1797
|
its validity; refresh_site extends validity without uploading.
|
|
1888
|
-
3.
|
|
1798
|
+
3. To make the site PERMANENT, subscribe it to a monthly hosting plan (subscribe_site ->
|
|
1889
1799
|
Stripe-hosted checkout; water/personal/share/business, auto-upgrade when the site outgrows
|
|
1890
|
-
its plan)
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
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.
|
|
1894
1807
|
|
|
1895
1808
|
Safety boundaries:
|
|
1896
1809
|
- Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
|
|
1897
1810
|
- Never upload source projects, secrets, .env files, private keys, archives, videos or audio.
|
|
1898
1811
|
- Payment card data is entered only on Stripe-hosted pages \u2014 never through the AI tool.
|
|
1899
|
-
- A subscription
|
|
1900
|
-
- 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.`;
|
|
1901
1815
|
function createSakupaMcpServer(opts) {
|
|
1902
1816
|
const client = opts.client ?? new HttpApiClient(new FetchTransport(opts.apiBaseUrl));
|
|
1903
1817
|
const server = new McpServer(
|
package/dist/index.js
CHANGED
|
@@ -1,59 +1,7 @@
|
|
|
1
1
|
// src/version.ts
|
|
2
|
-
var MCP_VERSION = "0.
|
|
2
|
+
var MCP_VERSION = "0.3.0";
|
|
3
3
|
var CLIENT_TYPE = "sakupa-mcp";
|
|
4
4
|
|
|
5
|
-
// src/transport.ts
|
|
6
|
-
var FetchTransport = class {
|
|
7
|
-
baseUrl;
|
|
8
|
-
constructor(baseUrl) {
|
|
9
|
-
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
10
|
-
}
|
|
11
|
-
async request(req) {
|
|
12
|
-
let url = `${this.baseUrl}${req.path}`;
|
|
13
|
-
if (req.query && Object.keys(req.query).length > 0) {
|
|
14
|
-
url += `?${new URLSearchParams(req.query).toString()}`;
|
|
15
|
-
}
|
|
16
|
-
const headers = {
|
|
17
|
-
accept: "application/json",
|
|
18
|
-
...req.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
19
|
-
...req.headers
|
|
20
|
-
};
|
|
21
|
-
const res = await fetch(url, {
|
|
22
|
-
method: req.method,
|
|
23
|
-
headers,
|
|
24
|
-
...req.body !== void 0 ? { body: req.body } : {}
|
|
25
|
-
});
|
|
26
|
-
const text2 = await res.text();
|
|
27
|
-
const responseHeaders = {};
|
|
28
|
-
res.headers.forEach((value, key) => {
|
|
29
|
-
responseHeaders[key] = value;
|
|
30
|
-
});
|
|
31
|
-
return {
|
|
32
|
-
status: res.status,
|
|
33
|
-
headers: responseHeaders,
|
|
34
|
-
...text2.length > 0 ? { body: text2 } : {}
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
async upload(target, body) {
|
|
38
|
-
if (target.url.startsWith("memory://")) {
|
|
39
|
-
throw new Error(
|
|
40
|
-
`Upload target "${target.url}" is an in-process memory URL. memory:// targets only exist inside the in-process test harness and cannot be uploaded to over HTTP.`
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
const res = await fetch(target.url, {
|
|
44
|
-
method: target.method,
|
|
45
|
-
headers: target.headers,
|
|
46
|
-
body
|
|
47
|
-
});
|
|
48
|
-
if (!res.ok) {
|
|
49
|
-
const text2 = await res.text().catch(() => "");
|
|
50
|
-
throw new Error(
|
|
51
|
-
`Upload of "${target.path}" failed with HTTP ${res.status}${text2 ? `: ${text2.slice(0, 200)}` : ""}`
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
|
|
57
5
|
// ../core/dist/domain/constants.js
|
|
58
6
|
var SERVICE_DOMAIN = "sakupa.com";
|
|
59
7
|
var FREE_SITE_URL_SUFFIX = `.${SERVICE_DOMAIN}`;
|
|
@@ -186,11 +134,11 @@ var HTTP_STATUS = {
|
|
|
186
134
|
conflict: 409,
|
|
187
135
|
state_conflict: 409,
|
|
188
136
|
rate_limited: 429,
|
|
189
|
-
insufficient_balance: 402,
|
|
190
137
|
payment_required: 402,
|
|
191
138
|
confirmation_required: 428,
|
|
192
139
|
dns_not_verified: 403,
|
|
193
140
|
not_deployable: 422,
|
|
141
|
+
upgrade_required: 426,
|
|
194
142
|
internal: 500
|
|
195
143
|
};
|
|
196
144
|
var SakupaError = class extends Error {
|
|
@@ -500,16 +448,70 @@ async function sha256Hex(bytes) {
|
|
|
500
448
|
}
|
|
501
449
|
|
|
502
450
|
// ../core/dist/domain/subscription.js
|
|
503
|
-
var SUBSCRIPTION_WARNING_TEXT = "You are subscribing {
|
|
451
|
+
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.";
|
|
504
452
|
|
|
505
453
|
// ../core/dist/dto.js
|
|
506
454
|
var CREDENTIAL_HEADER = "x-sakupa-credential";
|
|
507
455
|
var IDEMPOTENCY_HEADER = "x-sakupa-idempotency-key";
|
|
456
|
+
var MCP_VERSION_HEADER = "x-sakupa-mcp-version";
|
|
508
457
|
|
|
509
458
|
// ../core/dist/services/sites.js
|
|
510
459
|
var utf8Decoder = new TextDecoder("utf-8", { fatal: false });
|
|
511
460
|
var utf8Encoder = new TextEncoder();
|
|
512
461
|
|
|
462
|
+
// src/transport.ts
|
|
463
|
+
var FetchTransport = class {
|
|
464
|
+
baseUrl;
|
|
465
|
+
constructor(baseUrl) {
|
|
466
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
467
|
+
}
|
|
468
|
+
async request(req) {
|
|
469
|
+
let url = `${this.baseUrl}${req.path}`;
|
|
470
|
+
if (req.query && Object.keys(req.query).length > 0) {
|
|
471
|
+
url += `?${new URLSearchParams(req.query).toString()}`;
|
|
472
|
+
}
|
|
473
|
+
const headers = {
|
|
474
|
+
accept: "application/json",
|
|
475
|
+
[MCP_VERSION_HEADER]: MCP_VERSION,
|
|
476
|
+
...req.body !== void 0 ? { "content-type": "application/json" } : {},
|
|
477
|
+
...req.headers
|
|
478
|
+
};
|
|
479
|
+
const res = await fetch(url, {
|
|
480
|
+
method: req.method,
|
|
481
|
+
headers,
|
|
482
|
+
...req.body !== void 0 ? { body: req.body } : {}
|
|
483
|
+
});
|
|
484
|
+
const text2 = await res.text();
|
|
485
|
+
const responseHeaders = {};
|
|
486
|
+
res.headers.forEach((value, key) => {
|
|
487
|
+
responseHeaders[key] = value;
|
|
488
|
+
});
|
|
489
|
+
return {
|
|
490
|
+
status: res.status,
|
|
491
|
+
headers: responseHeaders,
|
|
492
|
+
...text2.length > 0 ? { body: text2 } : {}
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
async upload(target, body) {
|
|
496
|
+
if (target.url.startsWith("memory://")) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
`Upload target "${target.url}" is an in-process memory URL. memory:// targets only exist inside the in-process test harness and cannot be uploaded to over HTTP.`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
const res = await fetch(target.url, {
|
|
502
|
+
method: target.method,
|
|
503
|
+
headers: target.headers,
|
|
504
|
+
body
|
|
505
|
+
});
|
|
506
|
+
if (!res.ok) {
|
|
507
|
+
const text2 = await res.text().catch(() => "");
|
|
508
|
+
throw new Error(
|
|
509
|
+
`Upload of "${target.path}" failed with HTTP ${res.status}${text2 ? `: ${text2.slice(0, 200)}` : ""}`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
|
|
513
515
|
// src/api-client.ts
|
|
514
516
|
var KNOWN_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
515
517
|
"invalid_request",
|
|
@@ -520,11 +522,11 @@ var KNOWN_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
520
522
|
"conflict",
|
|
521
523
|
"state_conflict",
|
|
522
524
|
"rate_limited",
|
|
523
|
-
"insufficient_balance",
|
|
524
525
|
"payment_required",
|
|
525
526
|
"confirmation_required",
|
|
526
527
|
"dns_not_verified",
|
|
527
528
|
"not_deployable",
|
|
529
|
+
"upgrade_required",
|
|
528
530
|
"internal"
|
|
529
531
|
]);
|
|
530
532
|
var HttpApiClient = class {
|
|
@@ -625,14 +627,7 @@ var HttpApiClient = class {
|
|
|
625
627
|
{ body: req }
|
|
626
628
|
);
|
|
627
629
|
}
|
|
628
|
-
/**
|
|
629
|
-
async createCheckout(req) {
|
|
630
|
-
return this.call("POST", "/v1/payments/checkout", {
|
|
631
|
-
body: req,
|
|
632
|
-
idempotencyKey: req.idempotencyKey
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
/** Subscription checkout: a fixed monthly plan for one apex domain (owner-only). */
|
|
630
|
+
/** Subscription checkout: a fixed monthly plan permanentizing one site (owner-only). */
|
|
636
631
|
async createPlanCheckout(req, credential) {
|
|
637
632
|
return this.call("POST", "/v1/payments/checkout", {
|
|
638
633
|
credential,
|
|
@@ -641,31 +636,24 @@ var HttpApiClient = class {
|
|
|
641
636
|
});
|
|
642
637
|
}
|
|
643
638
|
/** Stripe-hosted billing portal (payment method, invoices, cancellation). */
|
|
644
|
-
async createBillingPortal(
|
|
639
|
+
async createBillingPortal(siteId, credential) {
|
|
645
640
|
return this.call(
|
|
646
641
|
"POST",
|
|
647
|
-
`/v1/
|
|
642
|
+
`/v1/sites/${encodeURIComponent(siteId)}/billing/portal`,
|
|
648
643
|
{ credential, body: {} }
|
|
649
644
|
);
|
|
650
645
|
}
|
|
651
|
-
async getBillingStatus(
|
|
646
|
+
async getBillingStatus(siteId, credential) {
|
|
652
647
|
return this.call(
|
|
653
648
|
"GET",
|
|
654
|
-
`/v1/
|
|
655
|
-
|
|
656
|
-
);
|
|
657
|
-
}
|
|
658
|
-
async setBillingPlan(apexDomain, credential, req) {
|
|
659
|
-
return this.call(
|
|
660
|
-
"POST",
|
|
661
|
-
`/v1/billing/${encodeURIComponent(apexDomain)}/plan`,
|
|
662
|
-
{ credential, body: req }
|
|
649
|
+
`/v1/sites/${encodeURIComponent(siteId)}/billing`,
|
|
650
|
+
{ credential }
|
|
663
651
|
);
|
|
664
652
|
}
|
|
665
|
-
async
|
|
653
|
+
async setBillingPlan(siteId, credential, req) {
|
|
666
654
|
return this.call(
|
|
667
655
|
"POST",
|
|
668
|
-
`/v1/sites/${encodeURIComponent(siteId)}/
|
|
656
|
+
`/v1/sites/${encodeURIComponent(siteId)}/billing/plan`,
|
|
669
657
|
{ credential, body: req }
|
|
670
658
|
);
|
|
671
659
|
}
|
|
@@ -711,8 +699,7 @@ function readSiteFile(projectDir) {
|
|
|
711
699
|
apiBaseUrl: typeof parsed.apiBaseUrl === "string" ? parsed.apiBaseUrl : "",
|
|
712
700
|
...typeof parsed.shortId === "string" ? { shortId: parsed.shortId } : {},
|
|
713
701
|
...typeof parsed.url === "string" ? { url: parsed.url } : {},
|
|
714
|
-
...typeof parsed.
|
|
715
|
-
...typeof parsed.billingDomainId === "string" ? { billingDomainId: parsed.billingDomainId } : {}
|
|
702
|
+
...typeof parsed.boundDomain === "string" ? { boundDomain: parsed.boundDomain } : {}
|
|
716
703
|
};
|
|
717
704
|
} catch {
|
|
718
705
|
return null;
|
|
@@ -1203,8 +1190,8 @@ var severityEnum = z.enum(["low", "medium", "high", "critical"]);
|
|
|
1203
1190
|
function planCatalog() {
|
|
1204
1191
|
return TIER_ORDER.map((p) => `${p} \xA5${tierPriceJpy(p)}/month`).join(", ");
|
|
1205
1192
|
}
|
|
1206
|
-
function subscriptionWarning(
|
|
1207
|
-
return SUBSCRIPTION_WARNING_TEXT.replaceAll("{
|
|
1193
|
+
function subscriptionWarning(siteUrl, plan) {
|
|
1194
|
+
return SUBSCRIPTION_WARNING_TEXT.replaceAll("{siteUrl}", siteUrl).replaceAll("{plan}", plan).replaceAll("{priceJpy}", String(tierPriceJpy(plan)));
|
|
1208
1195
|
}
|
|
1209
1196
|
var ticketCategoryEnum = z.enum([
|
|
1210
1197
|
"billing",
|
|
@@ -1257,7 +1244,7 @@ function ensureUploadSizeWithinLimits(manifest, isFirstFreeDeploy) {
|
|
|
1257
1244
|
if (total > FREE_SITE_MAX_TOTAL_BYTES) {
|
|
1258
1245
|
throw new SakupaError(
|
|
1259
1246
|
"validation_failed",
|
|
1260
|
-
`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
|
|
1247
|
+
`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.`
|
|
1261
1248
|
);
|
|
1262
1249
|
}
|
|
1263
1250
|
}
|
|
@@ -1326,7 +1313,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1326
1313
|
server.registerTool(
|
|
1327
1314
|
"deploy_site",
|
|
1328
1315
|
{
|
|
1329
|
-
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
|
|
1316
|
+
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.`,
|
|
1330
1317
|
inputSchema: {
|
|
1331
1318
|
outputDir: z.string().optional().describe("Output directory relative to the project root (overrides detection)."),
|
|
1332
1319
|
spaFallback: z.boolean().optional().describe("Enable SPA fallback (requires spaFallbackConfirmed: true)."),
|
|
@@ -1378,7 +1365,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1378
1365
|
Files uploaded: ${uploaded2} (${finalized2.totalBytes} bytes)
|
|
1379
1366
|
` + (finalized2.expiresAt ? `Expires at: ${finalized2.expiresAt}
|
|
1380
1367
|
` : "") + `
|
|
1381
|
-
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
|
|
1368
|
+
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.
|
|
1382
1369
|
` + (finalized2.warnings.length > 0 ? `
|
|
1383
1370
|
Warnings:
|
|
1384
1371
|
${JSON.stringify(finalized2.warnings, null, 2)}` : "")
|
|
@@ -1419,8 +1406,8 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : "")
|
|
|
1419
1406
|
Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
|
|
1420
1407
|
` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
|
|
1421
1408
|
` : "") + (finalized.mode === "free" ? `
|
|
1422
|
-
Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh_site call.
|
|
1423
|
-
` : "") + (finalized.warnings.length > 0 ? `
|
|
1409
|
+
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.
|
|
1410
|
+
` : "\nThis site is subscribed and permanent \u2014 no expiry.\n") + (finalized.warnings.length > 0 ? `
|
|
1424
1411
|
Warnings:
|
|
1425
1412
|
${JSON.stringify(finalized.warnings, null, 2)}` : "")
|
|
1426
1413
|
);
|
|
@@ -1432,7 +1419,7 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : "")
|
|
|
1432
1419
|
server.registerTool(
|
|
1433
1420
|
"refresh_site",
|
|
1434
1421
|
{
|
|
1435
|
-
description: "Refresh the validity of the free temporary site WITHOUT uploading content. Uses the local credential in .sakupa/site.json.",
|
|
1422
|
+
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.",
|
|
1436
1423
|
inputSchema: {}
|
|
1437
1424
|
},
|
|
1438
1425
|
async () => {
|
|
@@ -1451,7 +1438,7 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1451
1438
|
server.registerTool(
|
|
1452
1439
|
"site_status",
|
|
1453
1440
|
{
|
|
1454
|
-
description: "Show the current status of this project's Sakupa site: URL, mode (free/paid), expiry,
|
|
1441
|
+
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.",
|
|
1455
1442
|
inputSchema: {}
|
|
1456
1443
|
},
|
|
1457
1444
|
async () => {
|
|
@@ -1464,10 +1451,53 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1464
1451
|
}
|
|
1465
1452
|
}
|
|
1466
1453
|
);
|
|
1454
|
+
server.registerTool(
|
|
1455
|
+
"subscribe_site",
|
|
1456
|
+
{
|
|
1457
|
+
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.`,
|
|
1458
|
+
inputSchema: {
|
|
1459
|
+
plan: planEnum.describe(
|
|
1460
|
+
"Monthly plan: water (very light personal pages), personal (personal brand / small shop), share (small-business site), business (steadier traffic, more headroom)."
|
|
1461
|
+
),
|
|
1462
|
+
confirm: z.boolean().optional().describe("User read the subscription disclosure and confirmed. Required to proceed.")
|
|
1463
|
+
}
|
|
1464
|
+
},
|
|
1465
|
+
async (args) => {
|
|
1466
|
+
try {
|
|
1467
|
+
const site = requireSiteFile(ctx);
|
|
1468
|
+
const siteUrl = site.url ?? (site.shortId ? `https://${site.shortId}.sakupa.com` : site.siteId);
|
|
1469
|
+
if (args.confirm !== true) {
|
|
1470
|
+
return text(
|
|
1471
|
+
`${subscriptionWarning(siteUrl, 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_site with confirm: true.`
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
const res = await ctx.client.createPlanCheckout(
|
|
1477
|
+
{
|
|
1478
|
+
siteId: site.siteId,
|
|
1479
|
+
plan: args.plan,
|
|
1480
|
+
idempotencyKey: randomUUID(),
|
|
1481
|
+
confirmPlan: true
|
|
1482
|
+
},
|
|
1483
|
+
site.credential
|
|
1484
|
+
);
|
|
1485
|
+
return text(
|
|
1486
|
+
`Stripe Checkout link \u2014 Sakupa Hosting for this site: ${res.plan} plan, \xA5${res.monthlyPriceJpy}/month
|
|
1487
|
+
${res.checkoutUrl}
|
|
1488
|
+
|
|
1489
|
+
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.
|
|
1490
|
+
Once payment confirms, the site becomes permanent on its current URL. Binding a custom domain (bind_domain) is optional and still requires DNS verification.`
|
|
1491
|
+
);
|
|
1492
|
+
} catch (e) {
|
|
1493
|
+
return toolError(e);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
);
|
|
1467
1497
|
server.registerTool(
|
|
1468
1498
|
"bind_domain",
|
|
1469
1499
|
{
|
|
1470
|
-
description:
|
|
1500
|
+
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.",
|
|
1471
1501
|
inputSchema: {
|
|
1472
1502
|
hostname: z.string().describe('Hostname to bind, e.g. "example.com" or "www.example.com".'),
|
|
1473
1503
|
verificationId: z.string().optional().describe("Check an existing DNS verification instead of starting a new one.")
|
|
@@ -1479,12 +1509,12 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1479
1509
|
if (args.verificationId !== void 0) {
|
|
1480
1510
|
const res2 = await ctx.client.checkVerification(args.verificationId, site.credential);
|
|
1481
1511
|
if (res2.status === "verified") {
|
|
1482
|
-
writeSiteFile(ctx.projectDir, { ...site,
|
|
1512
|
+
writeSiteFile(ctx.projectDir, { ...site, boundDomain: res2.apexDomain });
|
|
1483
1513
|
}
|
|
1484
1514
|
return text(
|
|
1485
1515
|
`DNS verification ${res2.verificationId}: ${res2.status}
|
|
1486
1516
|
${res2.message}
|
|
1487
|
-
` + (res2.provisioningJobId ? `Provisioning started (job ${res2.provisioningJobId}). HTTPS certificate and
|
|
1517
|
+
` + (res2.provisioningJobId ? `Provisioning started (job ${res2.provisioningJobId}). HTTPS certificate and serving setup are in progress; check again with bind_domain + verificationId later.
|
|
1488
1518
|
` : "") + (res2.pendingDnsRecords.length > 0 ? `
|
|
1489
1519
|
DNS records still required:
|
|
1490
1520
|
${JSON.stringify(res2.pendingDnsRecords, null, 2)}` : "")
|
|
@@ -1495,22 +1525,15 @@ ${JSON.stringify(res2.pendingDnsRecords, null, 2)}` : "")
|
|
|
1495
1525
|
hostname: args.hostname
|
|
1496
1526
|
};
|
|
1497
1527
|
const res = await ctx.client.bindDomain(site.credential, req);
|
|
1498
|
-
writeSiteFile(ctx.projectDir, {
|
|
1499
|
-
...site,
|
|
1500
|
-
billingDomain: res.apexDomain,
|
|
1501
|
-
billingDomainId: res.billingDomainId
|
|
1502
|
-
});
|
|
1503
1528
|
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.`;
|
|
1504
1529
|
return text(
|
|
1505
1530
|
`Domain binding started for ${res.hostname}.
|
|
1506
1531
|
|
|
1507
|
-
Billing subject: the apex domain "${res.apexDomain}". Subdomains of ${res.apexDomain} cannot be separate billing subjects \u2014 they all share this domain hosting subscription.
|
|
1508
|
-
|
|
1509
1532
|
1. Prove control of ${res.apexDomain} by creating this DNS record:
|
|
1510
1533
|
name: ${res.verificationRecord.name}
|
|
1511
1534
|
type: ${res.verificationRecord.type}
|
|
1512
1535
|
value: ${res.verificationRecord.value}
|
|
1513
|
-
Ownership
|
|
1536
|
+
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.
|
|
1514
1537
|
|
|
1515
1538
|
2. Serving DNS (after verification): ${servingHint}
|
|
1516
1539
|
Server guidance: ${res.servingInstructions}
|
|
@@ -1525,33 +1548,24 @@ Then run bind_domain again with verificationId: "${res.verificationId}" to check
|
|
|
1525
1548
|
server.registerTool(
|
|
1526
1549
|
"billing_status",
|
|
1527
1550
|
{
|
|
1528
|
-
description: "Show
|
|
1529
|
-
inputSchema: {
|
|
1530
|
-
domain: z.string().optional().describe("Apex billing domain; defaults to the domain bound in .sakupa/site.json.")
|
|
1531
|
-
}
|
|
1551
|
+
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).",
|
|
1552
|
+
inputSchema: {}
|
|
1532
1553
|
},
|
|
1533
|
-
async (
|
|
1554
|
+
async () => {
|
|
1534
1555
|
try {
|
|
1535
|
-
const site =
|
|
1536
|
-
const
|
|
1537
|
-
if (!domain) {
|
|
1538
|
-
throw new SakupaError(
|
|
1539
|
-
"invalid_request",
|
|
1540
|
-
'No domain given and this project has no bound billing domain. Pass domain, e.g. billing_status { domain: "example.com" }.'
|
|
1541
|
-
);
|
|
1542
|
-
}
|
|
1543
|
-
const res = await ctx.client.getBillingStatus(domain, site?.credential);
|
|
1556
|
+
const site = requireSiteFile(ctx);
|
|
1557
|
+
const res = await ctx.client.getBillingStatus(site.siteId, site.credential);
|
|
1544
1558
|
const lines = [
|
|
1545
|
-
`Billing status for ${res.
|
|
1546
|
-
res.
|
|
1559
|
+
`Billing status for site ${res.siteId} (mode: ${res.mode})`,
|
|
1560
|
+
res.permanentUrl ? `Permanent URL: ${res.permanentUrl}` : void 0,
|
|
1561
|
+
res.plan ? `Plan: ${res.plan} (\xA5${res.monthlyPriceJpy ?? tierPriceJpy(res.plan)}/month)` : "Plan: (no subscription yet)",
|
|
1547
1562
|
res.subscriptionStatus ? `Subscription payment state: ${res.subscriptionStatus}` : void 0,
|
|
1548
|
-
res.
|
|
1549
|
-
res.
|
|
1563
|
+
res.cancelAtPeriodEnd ? "Renewal: CANCELED \u2014 the site reverts to free at the end of the already-paid month" : void 0,
|
|
1564
|
+
res.currentPeriodStart ? `Current paid period: ${res.currentPeriodStart} -> ${res.currentPeriodEnd ?? "?"}` : void 0,
|
|
1550
1565
|
res.estimatedUsageTier ? `Estimated usage tier: ${res.estimatedUsageTier}` : void 0,
|
|
1551
1566
|
res.lastReconciledAt ? `Last usage reconciliation: ${res.lastReconciledAt}` : void 0,
|
|
1552
|
-
res.
|
|
1553
|
-
res.risks.
|
|
1554
|
-
res.ownerView ? void 0 : "Note: public view (no valid owner credential presented)."
|
|
1567
|
+
res.boundHostnames.length > 0 ? `Bound custom domains: ${res.boundHostnames.join(", ")}` : void 0,
|
|
1568
|
+
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
|
|
1555
1569
|
].filter((l) => l !== void 0);
|
|
1556
1570
|
return textJson(`${lines.join("\n")}
|
|
1557
1571
|
|
|
@@ -1561,71 +1575,18 @@ Full status:`, res);
|
|
|
1561
1575
|
}
|
|
1562
1576
|
}
|
|
1563
1577
|
);
|
|
1564
|
-
server.registerTool(
|
|
1565
|
-
"subscribe_domain",
|
|
1566
|
-
{
|
|
1567
|
-
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.`,
|
|
1568
|
-
inputSchema: {
|
|
1569
|
-
domain: z.string().describe('Apex domain to subscribe, e.g. "example.com".'),
|
|
1570
|
-
plan: planEnum.describe(
|
|
1571
|
-
"Monthly plan: water (very light personal pages), personal (personal brand / small shop), share (small-business site), business (steadier traffic, more headroom)."
|
|
1572
|
-
),
|
|
1573
|
-
confirm: z.boolean().optional().describe("User read the subscription disclosure and confirmed. Required to proceed.")
|
|
1574
|
-
}
|
|
1575
|
-
},
|
|
1576
|
-
async (args) => {
|
|
1577
|
-
try {
|
|
1578
|
-
const site = requireSiteFile(ctx);
|
|
1579
|
-
if (args.confirm !== true) {
|
|
1580
|
-
return text(
|
|
1581
|
-
`${subscriptionWarning(args.domain, args.plan)}
|
|
1582
|
-
|
|
1583
|
-
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.`
|
|
1584
|
-
);
|
|
1585
|
-
}
|
|
1586
|
-
const res = await ctx.client.createPlanCheckout(
|
|
1587
|
-
{
|
|
1588
|
-
siteId: site.siteId,
|
|
1589
|
-
domain: args.domain,
|
|
1590
|
-
plan: args.plan,
|
|
1591
|
-
idempotencyKey: randomUUID(),
|
|
1592
|
-
confirmPlan: true
|
|
1593
|
-
},
|
|
1594
|
-
site.credential
|
|
1595
|
-
);
|
|
1596
|
-
return text(
|
|
1597
|
-
`Stripe Checkout link \u2014 Sakupa Domain Hosting for ${res.apexDomain}: ${res.plan} plan, \xA5${res.monthlyPriceJpy}/month
|
|
1598
|
-
${res.checkoutUrl}
|
|
1599
|
-
|
|
1600
|
-
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.
|
|
1601
|
-
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.`
|
|
1602
|
-
);
|
|
1603
|
-
} catch (e) {
|
|
1604
|
-
return toolError(e);
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
);
|
|
1608
1578
|
server.registerTool(
|
|
1609
1579
|
"manage_billing",
|
|
1610
1580
|
{
|
|
1611
|
-
description: "Open the Stripe-hosted billing portal for
|
|
1612
|
-
inputSchema: {
|
|
1613
|
-
domain: z.string().optional().describe("Apex billing domain; defaults to the domain bound in .sakupa/site.json.")
|
|
1614
|
-
}
|
|
1581
|
+
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).",
|
|
1582
|
+
inputSchema: {}
|
|
1615
1583
|
},
|
|
1616
|
-
async (
|
|
1584
|
+
async () => {
|
|
1617
1585
|
try {
|
|
1618
1586
|
const site = requireSiteFile(ctx);
|
|
1619
|
-
const
|
|
1620
|
-
if (!domain) {
|
|
1621
|
-
throw new SakupaError(
|
|
1622
|
-
"invalid_request",
|
|
1623
|
-
"No domain given and this project has no bound billing domain. Pass domain explicitly."
|
|
1624
|
-
);
|
|
1625
|
-
}
|
|
1626
|
-
const res = await ctx.client.createBillingPortal(domain, site.credential);
|
|
1587
|
+
const res = await ctx.client.createBillingPortal(site.siteId, site.credential);
|
|
1627
1588
|
return text(
|
|
1628
|
-
`Stripe billing portal for
|
|
1589
|
+
`Stripe billing portal for this site:
|
|
1629
1590
|
${res.portalUrl}
|
|
1630
1591
|
|
|
1631
1592
|
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.`
|
|
@@ -1638,7 +1599,7 @@ Open this link in a browser to update the payment method, view invoices, or mana
|
|
|
1638
1599
|
server.registerTool(
|
|
1639
1600
|
"recover_domain_site",
|
|
1640
1601
|
{
|
|
1641
|
-
description: "Recover management control of a
|
|
1602
|
+
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).",
|
|
1642
1603
|
inputSchema: {
|
|
1643
1604
|
hostname: z.string().describe('Hostname of the site to recover, e.g. "www.example.com".'),
|
|
1644
1605
|
verificationId: z.string().optional().describe("Complete a recovery previously started for this hostname."),
|
|
@@ -1650,7 +1611,7 @@ Open this link in a browser to update the payment method, view invoices, or mana
|
|
|
1650
1611
|
if (args.verificationId === void 0) {
|
|
1651
1612
|
const res2 = await ctx.client.recoverDomain({ hostname: args.hostname });
|
|
1652
1613
|
return text(
|
|
1653
|
-
`Recovery started for ${args.hostname} (apex
|
|
1614
|
+
`Recovery started for ${args.hostname} (apex domain: ${res2.apexDomain}).
|
|
1654
1615
|
|
|
1655
1616
|
Create this DNS record to prove apex-domain control:
|
|
1656
1617
|
name: ${res2.verificationRecord.name}
|
|
@@ -1669,13 +1630,13 @@ After the DNS record resolves, re-run recover_domain_site with verificationId: "
|
|
|
1669
1630
|
});
|
|
1670
1631
|
writeSiteFile(ctx.projectDir, {
|
|
1671
1632
|
siteId: res.siteId,
|
|
1672
|
-
|
|
1633
|
+
...res.boundHostnames[0] !== void 0 ? { boundDomain: res.boundHostnames[0] } : {},
|
|
1673
1634
|
credential: res.credential,
|
|
1674
1635
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1675
1636
|
apiBaseUrl: ctx.apiBaseUrl
|
|
1676
1637
|
});
|
|
1677
1638
|
return text(
|
|
1678
|
-
`Recovery complete
|
|
1639
|
+
`Recovery complete.
|
|
1679
1640
|
Site: ${res.siteId} (hostnames: ${res.boundHostnames.join(", ") || "(none)"})
|
|
1680
1641
|
Previous credentials revoked: ${res.revokedPreviousCredentials ? "YES" : "no (preserved on request)"}
|
|
1681
1642
|
|
|
@@ -1692,39 +1653,32 @@ ${res.archiveUrl}`
|
|
|
1692
1653
|
server.registerTool(
|
|
1693
1654
|
"set_billing_plan",
|
|
1694
1655
|
{
|
|
1695
|
-
description: `Change
|
|
1656
|
+
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.`,
|
|
1696
1657
|
inputSchema: {
|
|
1697
|
-
domain: z.string().optional().describe("Apex billing domain; defaults to the domain bound in .sakupa/site.json."),
|
|
1698
1658
|
plan: planEnum.optional().describe("Target monthly plan."),
|
|
1699
1659
|
cancelRenewal: z.boolean().optional().describe(
|
|
1700
|
-
"true: cancel renewal (
|
|
1660
|
+
"true: cancel renewal (the site stays permanent to the end of the paid month, then reverts to free). false: re-enable renewal."
|
|
1701
1661
|
),
|
|
1662
|
+
cancelNow: z.boolean().optional().describe("End the subscription immediately; the site reverts to free right away."),
|
|
1702
1663
|
confirm: z.boolean().optional().describe("User saw the consequences and explicitly confirmed.")
|
|
1703
1664
|
}
|
|
1704
1665
|
},
|
|
1705
1666
|
async (args) => {
|
|
1706
1667
|
try {
|
|
1707
1668
|
const site = requireSiteFile(ctx);
|
|
1708
|
-
const domain = args.domain ?? site.billingDomain;
|
|
1709
|
-
if (!domain) {
|
|
1710
|
-
throw new SakupaError(
|
|
1711
|
-
"invalid_request",
|
|
1712
|
-
"No domain given and this project has no bound billing domain. Pass domain explicitly."
|
|
1713
|
-
);
|
|
1714
|
-
}
|
|
1715
1669
|
const req = {
|
|
1716
|
-
|
|
1717
|
-
...args.plan !== void 0 ? { committedTier: args.plan } : {},
|
|
1670
|
+
...args.plan !== void 0 ? { plan: args.plan } : {},
|
|
1718
1671
|
...args.cancelRenewal !== void 0 ? { cancelRenewal: args.cancelRenewal } : {},
|
|
1672
|
+
...args.cancelNow !== void 0 ? { cancelNow: args.cancelNow } : {},
|
|
1719
1673
|
confirm: args.confirm === true
|
|
1720
1674
|
};
|
|
1721
1675
|
try {
|
|
1722
|
-
const res = await ctx.client.setBillingPlan(
|
|
1676
|
+
const res = await ctx.client.setBillingPlan(site.siteId, site.credential, req);
|
|
1723
1677
|
return textJson(
|
|
1724
|
-
`Billing
|
|
1678
|
+
`Billing updated for site ${res.siteId} (mode: ${res.mode}).
|
|
1725
1679
|
Consequences:
|
|
1726
1680
|
${res.consequences.map((c) => `- ${c}`).join("\n")}
|
|
1727
|
-
|
|
1681
|
+
Result:`,
|
|
1728
1682
|
res
|
|
1729
1683
|
);
|
|
1730
1684
|
} catch (e) {
|
|
@@ -1744,50 +1698,6 @@ ${e.message}
|
|
|
1744
1698
|
}
|
|
1745
1699
|
}
|
|
1746
1700
|
);
|
|
1747
|
-
server.registerTool(
|
|
1748
|
-
"downgrade_to_free",
|
|
1749
|
-
{
|
|
1750
|
-
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.",
|
|
1751
|
-
inputSchema: {
|
|
1752
|
-
immediateStopServing: z.boolean().optional().describe(
|
|
1753
|
-
"Also stop custom-domain serving immediately (does not remove current-cycle charges)."
|
|
1754
|
-
),
|
|
1755
|
-
confirm: z.boolean().optional().describe("User saw the consequences and explicitly confirmed.")
|
|
1756
|
-
}
|
|
1757
|
-
},
|
|
1758
|
-
async (args) => {
|
|
1759
|
-
try {
|
|
1760
|
-
const site = requireSiteFile(ctx);
|
|
1761
|
-
try {
|
|
1762
|
-
const res = await ctx.client.downgradeSite(site.siteId, site.credential, {
|
|
1763
|
-
...args.immediateStopServing !== void 0 ? { immediateStopServing: args.immediateStopServing } : {},
|
|
1764
|
-
confirm: args.confirm === true
|
|
1765
|
-
});
|
|
1766
|
-
return text(
|
|
1767
|
-
`Downgrade scheduled for site ${res.siteId}.
|
|
1768
|
-
Effective at: ${res.effectiveAt}
|
|
1769
|
-
Immediate stop-serving applied: ${res.immediateStopApplied ? "yes" : "no"}
|
|
1770
|
-
${res.message}
|
|
1771
|
-
|
|
1772
|
-
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).`
|
|
1773
|
-
);
|
|
1774
|
-
} catch (e) {
|
|
1775
|
-
if (isSakupaError(e) && e.code === "confirmation_required") {
|
|
1776
|
-
return text(
|
|
1777
|
-
`Confirmation required before downgrading \u2014 nothing was applied.
|
|
1778
|
-
|
|
1779
|
-
${e.message}
|
|
1780
|
-
` + (e.details !== void 0 ? `${JSON.stringify(e.details, null, 2)}
|
|
1781
|
-
` : "") + "\nPlease show these consequences to the user and, after their explicit confirmation, re-run downgrade_to_free with confirm: true."
|
|
1782
|
-
);
|
|
1783
|
-
}
|
|
1784
|
-
throw e;
|
|
1785
|
-
}
|
|
1786
|
-
} catch (e) {
|
|
1787
|
-
return toolError(e);
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
);
|
|
1791
1701
|
server.registerTool(
|
|
1792
1702
|
"create_support_ticket",
|
|
1793
1703
|
{
|
|
@@ -1818,7 +1728,7 @@ ${e.message}
|
|
|
1818
1728
|
server.registerTool(
|
|
1819
1729
|
"report_bug",
|
|
1820
1730
|
{
|
|
1821
|
-
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,
|
|
1731
|
+
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.",
|
|
1822
1732
|
inputSchema: {
|
|
1823
1733
|
toolName: z.string().describe('The Sakupa tool that failed, e.g. "deploy_site".'),
|
|
1824
1734
|
errorCode: z.string().optional(),
|
|
@@ -1838,7 +1748,7 @@ ${e.message}
|
|
|
1838
1748
|
...args.errorCode !== void 0 ? { errorCode: args.errorCode } : {},
|
|
1839
1749
|
...args.errorMessage !== void 0 ? { sanitizedErrorMessage: args.errorMessage } : {},
|
|
1840
1750
|
...site ? { siteId: site.siteId } : {},
|
|
1841
|
-
...site?.
|
|
1751
|
+
...site?.boundDomain ? { boundDomain: site.boundDomain } : {},
|
|
1842
1752
|
...args.deploymentId !== void 0 ? { deploymentId: args.deploymentId } : {},
|
|
1843
1753
|
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1844
1754
|
clientType: CLIENT_TYPE,
|
|
@@ -1881,19 +1791,23 @@ Workflow:
|
|
|
1881
1791
|
site (public URL {shortId}.sakupa.com, valid 24 hours, free banner shown) and stores the
|
|
1882
1792
|
management credential in .sakupa/site.json. Deploying again updates the site and refreshes
|
|
1883
1793
|
its validity; refresh_site extends validity without uploading.
|
|
1884
|
-
3.
|
|
1794
|
+
3. To make the site PERMANENT, subscribe it to a monthly hosting plan (subscribe_site ->
|
|
1885
1795
|
Stripe-hosted checkout; water/personal/share/business, auto-upgrade when the site outgrows
|
|
1886
|
-
its plan)
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1796
|
+
its plan). Paying makes the {shortId}.sakupa.com URL permanent \u2014 that is what payment buys.
|
|
1797
|
+
4. Optionally bind a custom domain to the subscribed site (bind_domain): an included extra
|
|
1798
|
+
serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
|
|
1799
|
+
first verified request wins; unverified requests expire after 72 hours. billing_status,
|
|
1800
|
+
set_billing_plan, manage_billing and recover_domain_site manage the paid lifecycle.
|
|
1801
|
+
Canceling the subscription immediately reverts the site to a free 24h site and removes all
|
|
1802
|
+
paid data.
|
|
1890
1803
|
|
|
1891
1804
|
Safety boundaries:
|
|
1892
1805
|
- Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
|
|
1893
1806
|
- Never upload source projects, secrets, .env files, private keys, archives, videos or audio.
|
|
1894
1807
|
- Payment card data is entered only on Stripe-hosted pages \u2014 never through the AI tool.
|
|
1895
|
-
- A subscription
|
|
1896
|
-
- The management credential lives only in .sakupa/site.json; never share or upload it
|
|
1808
|
+
- A subscription never grants domain ownership; only DNS verification does.
|
|
1809
|
+
- The management credential lives only in .sakupa/site.json; never share or upload it. Without
|
|
1810
|
+
a bound custom domain, a lost credential is unrecoverable by design.`;
|
|
1897
1811
|
function createSakupaMcpServer(opts) {
|
|
1898
1812
|
const client = opts.client ?? new HttpApiClient(new FetchTransport(opts.apiBaseUrl));
|
|
1899
1813
|
const server = new McpServer(
|