@sakupa/mcp 0.5.0 → 0.7.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 +594 -177
- package/dist/index.js +591 -177
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -127,7 +127,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
127
127
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
128
128
|
|
|
129
129
|
// ../core/dist/domain/version.js
|
|
130
|
-
var SAKUPA_MCP_VERSION = "0.
|
|
130
|
+
var SAKUPA_MCP_VERSION = "0.7.0";
|
|
131
131
|
|
|
132
132
|
// ../core/dist/domain/errors.js
|
|
133
133
|
var HTTP_STATUS = {
|
|
@@ -452,9 +452,6 @@ async function sha256Hex(bytes) {
|
|
|
452
452
|
return hex;
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
-
// ../core/dist/domain/subscription.js
|
|
456
|
-
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.";
|
|
457
|
-
|
|
458
455
|
// ../core/dist/dto.js
|
|
459
456
|
var CREDENTIAL_HEADER = "x-sakupa-credential";
|
|
460
457
|
var IDEMPOTENCY_HEADER = "x-sakupa-idempotency-key";
|
|
@@ -467,9 +464,6 @@ var utf8Encoder = new TextEncoder();
|
|
|
467
464
|
// ../core/dist/services/subscriptions.js
|
|
468
465
|
var WEBHOOK_PROCESSING_LEASE_MS = 5 * 60 * 1e3;
|
|
469
466
|
|
|
470
|
-
// ../core/dist/services/billing-cancellation.js
|
|
471
|
-
var encoder = new TextEncoder();
|
|
472
|
-
|
|
473
467
|
// src/server.ts
|
|
474
468
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
475
469
|
|
|
@@ -565,8 +559,18 @@ var HttpApiClient = class {
|
|
|
565
559
|
credential
|
|
566
560
|
});
|
|
567
561
|
}
|
|
568
|
-
async deleteSite(siteId, credential) {
|
|
569
|
-
|
|
562
|
+
async deleteSite(siteId, credential, req) {
|
|
563
|
+
return this.call("POST", `/v1/sites/${encodeURIComponent(siteId)}/delete`, {
|
|
564
|
+
credential,
|
|
565
|
+
body: req
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
async previewDeleteSite(siteId, credential, req) {
|
|
569
|
+
return this.call(
|
|
570
|
+
"POST",
|
|
571
|
+
`/v1/sites/${encodeURIComponent(siteId)}/delete/preview`,
|
|
572
|
+
{ credential, body: req }
|
|
573
|
+
);
|
|
570
574
|
}
|
|
571
575
|
async bindDomain(credential, req) {
|
|
572
576
|
return this.call("POST", "/v1/domains/bind", { credential, body: req });
|
|
@@ -578,9 +582,29 @@ var HttpApiClient = class {
|
|
|
578
582
|
{ credential }
|
|
579
583
|
);
|
|
580
584
|
}
|
|
585
|
+
async unbindDomain(siteId, credential, req) {
|
|
586
|
+
return this.call(
|
|
587
|
+
"POST",
|
|
588
|
+
`/v1/sites/${encodeURIComponent(siteId)}/domain/unbind`,
|
|
589
|
+
{ credential, body: req }
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
async previewUnbindDomain(siteId, credential, req) {
|
|
593
|
+
return this.call(
|
|
594
|
+
"POST",
|
|
595
|
+
`/v1/sites/${encodeURIComponent(siteId)}/domain/unbind/preview`,
|
|
596
|
+
{ credential, body: req }
|
|
597
|
+
);
|
|
598
|
+
}
|
|
581
599
|
async recoverDomain(req) {
|
|
582
600
|
return this.call("POST", "/v1/domains/recover", { body: req });
|
|
583
601
|
}
|
|
602
|
+
async getRecoveryStatus(verificationId) {
|
|
603
|
+
return this.call(
|
|
604
|
+
"GET",
|
|
605
|
+
`/v1/domains/recover/${encodeURIComponent(verificationId)}`
|
|
606
|
+
);
|
|
607
|
+
}
|
|
584
608
|
async completeRecovery(verificationId, req) {
|
|
585
609
|
return this.call(
|
|
586
610
|
"POST",
|
|
@@ -611,19 +635,18 @@ var HttpApiClient = class {
|
|
|
611
635
|
{ credential }
|
|
612
636
|
);
|
|
613
637
|
}
|
|
614
|
-
async
|
|
638
|
+
async getBillingPlanCatalog() {
|
|
639
|
+
return this.call("GET", "/v1/billing/plans");
|
|
640
|
+
}
|
|
641
|
+
async changeSubscriptionPlan(credential, req) {
|
|
615
642
|
return this.call(
|
|
616
643
|
"POST",
|
|
617
|
-
`/v1/sites/${encodeURIComponent(siteId)}/billing/plan`,
|
|
644
|
+
`/v1/sites/${encodeURIComponent(req.siteId)}/billing/plan-change`,
|
|
618
645
|
{ credential, body: req }
|
|
619
646
|
);
|
|
620
647
|
}
|
|
621
|
-
async
|
|
622
|
-
return this.call(
|
|
623
|
-
"POST",
|
|
624
|
-
"/v1/billing/cancellation-requests",
|
|
625
|
-
{ body: req }
|
|
626
|
-
);
|
|
648
|
+
async getPublicBillingPortal() {
|
|
649
|
+
return this.call("GET", "/v1/billing/portal");
|
|
627
650
|
}
|
|
628
651
|
async createTicket(credential, req) {
|
|
629
652
|
return this.call("POST", "/v1/support/tickets", {
|
|
@@ -643,7 +666,7 @@ var HttpApiClient = class {
|
|
|
643
666
|
import { randomUUID } from "node:crypto";
|
|
644
667
|
import { promises as fs2 } from "node:fs";
|
|
645
668
|
import { join as join3, resolve as resolve2 } from "node:path";
|
|
646
|
-
import { z } from "zod";
|
|
669
|
+
import { z as z2 } from "zod";
|
|
647
670
|
|
|
648
671
|
// src/analyze/analyzer.ts
|
|
649
672
|
import { promises as fs } from "node:fs";
|
|
@@ -1116,6 +1139,12 @@ function writeSiteFile(projectDir, file) {
|
|
|
1116
1139
|
} catch {
|
|
1117
1140
|
}
|
|
1118
1141
|
}
|
|
1142
|
+
function deleteSiteFile(projectDir) {
|
|
1143
|
+
const path = siteFilePath(projectDir);
|
|
1144
|
+
if (existsSync(path)) {
|
|
1145
|
+
rmSync(path, { force: true });
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1119
1148
|
function isInsideGitRepo(projectDir) {
|
|
1120
1149
|
let dir = projectDir;
|
|
1121
1150
|
for (; ; ) {
|
|
@@ -1134,6 +1163,47 @@ function credentialGitReminder(projectDir) {
|
|
|
1134
1163
|
var MCP_VERSION = SAKUPA_MCP_VERSION;
|
|
1135
1164
|
var CLIENT_TYPE = "sakupa-mcp";
|
|
1136
1165
|
|
|
1166
|
+
// src/tools/result.ts
|
|
1167
|
+
import { z } from "zod";
|
|
1168
|
+
var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
|
|
1169
|
+
schemaVersion: z.literal(1),
|
|
1170
|
+
outcome: z.enum([
|
|
1171
|
+
"completed",
|
|
1172
|
+
"preview",
|
|
1173
|
+
"waiting_user",
|
|
1174
|
+
"pending_provider",
|
|
1175
|
+
"blocked",
|
|
1176
|
+
"expired",
|
|
1177
|
+
"failed"
|
|
1178
|
+
]),
|
|
1179
|
+
resultCode: z.string(),
|
|
1180
|
+
operationId: z.string().optional(),
|
|
1181
|
+
summary: z.string(),
|
|
1182
|
+
data: z.record(z.string(), z.unknown()),
|
|
1183
|
+
userAction: z.object({
|
|
1184
|
+
type: z.enum(["open_url", "confirm_in_mcp", "configure_dns"]),
|
|
1185
|
+
provider: z.enum(["stripe", "sakupa"]).optional(),
|
|
1186
|
+
url: z.string().optional(),
|
|
1187
|
+
expiresAt: z.string().optional(),
|
|
1188
|
+
expectedOutcome: z.string(),
|
|
1189
|
+
resumeWith: z.object({ tool: z.string(), arguments: z.record(z.string(), z.unknown()) }).optional()
|
|
1190
|
+
}).optional(),
|
|
1191
|
+
nextActions: z.array(
|
|
1192
|
+
z.object({
|
|
1193
|
+
tool: z.string(),
|
|
1194
|
+
arguments: z.record(z.string(), z.unknown()).optional(),
|
|
1195
|
+
allowed: z.boolean(),
|
|
1196
|
+
reasonCode: z.string().optional()
|
|
1197
|
+
})
|
|
1198
|
+
)
|
|
1199
|
+
};
|
|
1200
|
+
function structuredToolResult(envelope) {
|
|
1201
|
+
return {
|
|
1202
|
+
content: [{ type: "text", text: envelope.summary }],
|
|
1203
|
+
structuredContent: envelope
|
|
1204
|
+
};
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1137
1207
|
// src/tools/context.ts
|
|
1138
1208
|
function requireSiteFile(ctx) {
|
|
1139
1209
|
const file = readSiteFile(ctx.projectDir);
|
|
@@ -1146,35 +1216,67 @@ function requireSiteFile(ctx) {
|
|
|
1146
1216
|
return file;
|
|
1147
1217
|
}
|
|
1148
1218
|
function toolError(e) {
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1219
|
+
const errorCode = isSakupaError(e) ? e.code : "internal";
|
|
1220
|
+
const retryable = errorCode === "rate_limited" || errorCode === "internal";
|
|
1221
|
+
const safeDetailKeys = /* @__PURE__ */ new Set([
|
|
1222
|
+
"retryAfterSeconds",
|
|
1223
|
+
"reasonCode",
|
|
1224
|
+
"currentStatus",
|
|
1225
|
+
"expectedStatus",
|
|
1226
|
+
"minimumVersion",
|
|
1227
|
+
"currentVersion"
|
|
1228
|
+
]);
|
|
1229
|
+
const rawDetails = isSakupaError(e) && e.details && typeof e.details === "object" ? e.details : void 0;
|
|
1230
|
+
const safeDetails = rawDetails ? Object.fromEntries(
|
|
1231
|
+
Object.entries(rawDetails).filter(
|
|
1232
|
+
([key, value]) => safeDetailKeys.has(key) && (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
1233
|
+
)
|
|
1234
|
+
) : void 0;
|
|
1235
|
+
const safeSummary = errorCode === "not_found" ? "\u6240\u9700\u7684\u672C\u5730\u9879\u76EE\u7ED1\u5B9A\u6216\u8D44\u6E90\u4E0D\u53EF\u7528\uFF1B\u5982\u679C\u672C\u5730\u6CA1\u6709 .sakupa/site.json\uFF0C\u8BF7\u5148\u8FD0\u884C deploy_site first\u3002" : errorCode === "unauthorized" ? "\u5F53\u524D\u64CD\u4F5C\u672A\u901A\u8FC7\u7AD9\u70B9\u6743\u9650\u6821\u9A8C\u3002" : errorCode === "invalid_request" || errorCode === "validation_failed" ? "\u8BF7\u6C42\u53C2\u6570\u6216\u672C\u5730\u9879\u76EE\u68C0\u67E5\u672A\u901A\u8FC7\u3002" : errorCode === "state_conflict" ? "\u8D44\u6E90\u72B6\u6001\u5DF2\u7ECF\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u67E5\u8BE2\u72B6\u6001\u540E\u518D\u51B3\u5B9A\u4E0B\u4E00\u6B65\u3002" : errorCode === "confirmation_required" ? "\u8D44\u6E90\u6216\u8D26\u5355\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u65E7\u786E\u8BA4\u5DF2\u5931\u6548\uFF1B\u8BF7\u91CD\u65B0\u9884\u89C8\u540E\u518D\u786E\u8BA4\u3002" : errorCode === "payment_required" ? "\u8BE5\u64CD\u4F5C\u9700\u8981\u6709\u6548\u8BA2\u9605\uFF1B\u8BF7\u5148\u67E5\u8BE2\u8D26\u5355\u72B6\u6001\u3002" : errorCode === "rate_limited" ? "\u8BF7\u6C42\u9891\u7387\u5DF2\u8FBE\u5230\u670D\u52A1\u7AEF\u4E0A\u9650\uFF0C\u8BF7\u6309\u8FD4\u56DE\u7684\u7B49\u5F85\u65F6\u95F4\u540E\u91CD\u8BD5\u3002" : retryable ? "\u5916\u90E8\u670D\u52A1\u6682\u65F6\u4E0D\u53EF\u7528\u6216\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002" : "\u64CD\u4F5C\u5931\u8D25\uFF1B\u672A\u8FD4\u56DE\u670D\u52A1\u7AEF\u5185\u90E8\u8BE6\u60C5\u3002";
|
|
1236
|
+
const result = structuredToolResult({
|
|
1237
|
+
schemaVersion: 1,
|
|
1238
|
+
outcome: "failed",
|
|
1239
|
+
resultCode: `error_${errorCode}`,
|
|
1240
|
+
summary: safeSummary,
|
|
1241
|
+
data: {
|
|
1242
|
+
errorCode,
|
|
1243
|
+
retryable,
|
|
1244
|
+
...safeDetails && Object.keys(safeDetails).length > 0 ? { details: safeDetails } : {}
|
|
1245
|
+
},
|
|
1246
|
+
nextActions: []
|
|
1247
|
+
});
|
|
1248
|
+
return { ...result, isError: true };
|
|
1159
1249
|
}
|
|
1160
1250
|
|
|
1161
1251
|
// src/tools/definitions.ts
|
|
1162
|
-
function text(t) {
|
|
1163
|
-
return {
|
|
1252
|
+
function text(resultCode, t, data = {}, outcome = "completed") {
|
|
1253
|
+
return structuredToolResult({
|
|
1254
|
+
schemaVersion: 1,
|
|
1255
|
+
outcome,
|
|
1256
|
+
resultCode,
|
|
1257
|
+
summary: t,
|
|
1258
|
+
data,
|
|
1259
|
+
nextActions: []
|
|
1260
|
+
});
|
|
1164
1261
|
}
|
|
1165
|
-
function textJson(header, obj) {
|
|
1166
|
-
|
|
1167
|
-
${JSON.stringify(obj, null, 2)}
|
|
1262
|
+
function textJson(resultCode, header, obj, outcome = "completed") {
|
|
1263
|
+
const summary = `${header}
|
|
1264
|
+
${JSON.stringify(obj, null, 2)}`;
|
|
1265
|
+
return structuredToolResult({
|
|
1266
|
+
schemaVersion: 1,
|
|
1267
|
+
outcome,
|
|
1268
|
+
resultCode,
|
|
1269
|
+
summary,
|
|
1270
|
+
data: typeof obj === "object" && obj !== null ? { result: obj } : { result: obj },
|
|
1271
|
+
nextActions: []
|
|
1272
|
+
});
|
|
1168
1273
|
}
|
|
1169
|
-
var planEnum =
|
|
1170
|
-
var severityEnum =
|
|
1274
|
+
var planEnum = z2.enum(["water", "personal", "share", "business"]);
|
|
1275
|
+
var severityEnum = z2.enum(["low", "medium", "high", "critical"]);
|
|
1171
1276
|
function planCatalog() {
|
|
1172
1277
|
return TIER_ORDER.map((p) => `${p} \xA5${tierPriceJpy(p)}/month`).join(", ");
|
|
1173
1278
|
}
|
|
1174
|
-
|
|
1175
|
-
return SUBSCRIPTION_WARNING_TEXT.replaceAll("{siteUrl}", siteUrl).replaceAll("{plan}", plan).replaceAll("{priceJpy}", String(tierPriceJpy(plan)));
|
|
1176
|
-
}
|
|
1177
|
-
var ticketCategoryEnum = z.enum([
|
|
1279
|
+
var ticketCategoryEnum = z2.enum([
|
|
1178
1280
|
"billing",
|
|
1179
1281
|
"payment",
|
|
1180
1282
|
"refund_review",
|
|
@@ -1192,14 +1294,17 @@ function analysisSummary(analysis) {
|
|
|
1192
1294
|
}
|
|
1193
1295
|
function notDeployableResult(analysis) {
|
|
1194
1296
|
return textJson(
|
|
1297
|
+
"site_analysis_not_deployable",
|
|
1195
1298
|
`This project is NOT deployable as-is. No files were uploaded and no API call was made.
|
|
1196
1299
|
Next action: ${analysis.suggestedNextAction}
|
|
1197
1300
|
Analysis:`,
|
|
1198
|
-
analysisSummary(analysis)
|
|
1301
|
+
analysisSummary(analysis),
|
|
1302
|
+
"blocked"
|
|
1199
1303
|
);
|
|
1200
1304
|
}
|
|
1201
1305
|
function spaConfirmationResult(analysis) {
|
|
1202
1306
|
return text(
|
|
1307
|
+
"spa_fallback_confirmation_required",
|
|
1203
1308
|
`SPA fallback confirmation required \u2014 nothing was deployed yet.
|
|
1204
1309
|
|
|
1205
1310
|
This site looks like a single-page application (one index.html plus JavaScript). SPA fallback rewrites every unknown path to index.html so client-side routes work, but it CHANGES normal 404 behavior: visitors never see a not-found page.
|
|
@@ -1208,7 +1313,9 @@ Please ask the user to choose, then re-run deploy_site with:
|
|
|
1208
1313
|
- spaFallback: true, spaFallbackConfirmed: true -> enable SPA fallback
|
|
1209
1314
|
- spaFallbackConfirmed: true (spaFallback omitted or false) -> deploy WITHOUT fallback (unknown paths return 404)
|
|
1210
1315
|
|
|
1211
|
-
Output directory: "${analysis.recommendedOutputDir ?? "."}", ${analysis.fileCount} files
|
|
1316
|
+
Output directory: "${analysis.recommendedOutputDir ?? "."}", ${analysis.fileCount} files.`,
|
|
1317
|
+
{ analysis: analysisSummary(analysis), requestedConfirmation: "spa_fallback" },
|
|
1318
|
+
"waiting_user"
|
|
1212
1319
|
);
|
|
1213
1320
|
}
|
|
1214
1321
|
var MB2 = 1024 * 1024;
|
|
@@ -1268,10 +1375,12 @@ function registerTools(server, ctx) {
|
|
|
1268
1375
|
"analyze_site",
|
|
1269
1376
|
{
|
|
1270
1377
|
description: "Analyze the local project and decide whether it can be deployed as a static site. Detects the framework, the built static output directory (dist/build/out/...), missing index.html, SSR/API-route/database-runtime risks, SPA fallback needs, forbidden files (secrets, .env, archives, media) and size limits. Sakupa deploys ONLY prebuilt static output \u2014 never source, secrets or server code. Run this before deploy_site.",
|
|
1378
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1379
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
1271
1380
|
inputSchema: {
|
|
1272
|
-
outputDir:
|
|
1273
|
-
spaFallbackRequested:
|
|
1274
|
-
spaFallbackConfirmed:
|
|
1381
|
+
outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection)."),
|
|
1382
|
+
spaFallbackRequested: z2.boolean().optional().describe("User asked for SPA fallback (unknown paths rewritten to index.html)."),
|
|
1383
|
+
spaFallbackConfirmed: z2.boolean().optional().describe("User explicitly confirmed the SPA fallback 404-behavior change.")
|
|
1275
1384
|
}
|
|
1276
1385
|
},
|
|
1277
1386
|
async (args) => {
|
|
@@ -1282,6 +1391,7 @@ function registerTools(server, ctx) {
|
|
|
1282
1391
|
...args.spaFallbackConfirmed !== void 0 ? { spaFallbackConfirmed: args.spaFallbackConfirmed } : {}
|
|
1283
1392
|
});
|
|
1284
1393
|
return textJson(
|
|
1394
|
+
"site_analysis_completed",
|
|
1285
1395
|
`Analysis of ${ctx.projectDir}
|
|
1286
1396
|
Next action: ${analysis.suggestedNextAction}`,
|
|
1287
1397
|
analysisSummary(analysis)
|
|
@@ -1295,14 +1405,16 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1295
1405
|
"deploy_site",
|
|
1296
1406
|
{
|
|
1297
1407
|
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.`,
|
|
1408
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1409
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1298
1410
|
inputSchema: {
|
|
1299
|
-
outputDir:
|
|
1300
|
-
spaFallback:
|
|
1301
|
-
spaFallbackConfirmed:
|
|
1302
|
-
publicConfirmed:
|
|
1411
|
+
outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection)."),
|
|
1412
|
+
spaFallback: z2.boolean().optional().describe("Enable SPA fallback (requires spaFallbackConfirmed: true)."),
|
|
1413
|
+
spaFallbackConfirmed: z2.boolean().optional().describe("User explicitly confirmed the SPA fallback 404-behavior change."),
|
|
1414
|
+
publicConfirmed: z2.boolean().optional().describe(
|
|
1303
1415
|
"Required only for the first deployment: user explicitly confirmed creation of a public 24-hour URL."
|
|
1304
1416
|
),
|
|
1305
|
-
lang:
|
|
1417
|
+
lang: z2.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
|
|
1306
1418
|
}
|
|
1307
1419
|
},
|
|
1308
1420
|
async (args) => {
|
|
@@ -1324,7 +1436,10 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1324
1436
|
const existing = readSiteFile(ctx.projectDir);
|
|
1325
1437
|
if (!existing && args.publicConfirmed !== true) {
|
|
1326
1438
|
return text(
|
|
1327
|
-
|
|
1439
|
+
"public_deployment_confirmation_required",
|
|
1440
|
+
`First deployment creates a public URL that anyone with the link can open. The free preview stays live for ${FREE_SITE_TTL_HOURS} hours. Explain this to the user and obtain explicit confirmation before retrying deploy_site with publicConfirmed: true.`,
|
|
1441
|
+
{ publicUrlLifetimeHours: FREE_SITE_TTL_HOURS, confirmationField: "publicConfirmed" },
|
|
1442
|
+
"waiting_user"
|
|
1328
1443
|
);
|
|
1329
1444
|
}
|
|
1330
1445
|
ensureUploadSizeWithinLimits(manifest, !existing);
|
|
@@ -1350,6 +1465,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
1350
1465
|
apiBaseUrl: ctx.apiBaseUrl
|
|
1351
1466
|
});
|
|
1352
1467
|
return text(
|
|
1468
|
+
"site_published",
|
|
1353
1469
|
`Site published: ${finalized2.url}
|
|
1354
1470
|
Files uploaded: ${uploaded2} (${finalized2.totalBytes} bytes)
|
|
1355
1471
|
` + (finalized2.expiresAt ? `Expires at: ${finalized2.expiresAt}
|
|
@@ -1357,7 +1473,19 @@ Files uploaded: ${uploaded2} (${finalized2.totalBytes} bytes)
|
|
|
1357
1473
|
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.
|
|
1358
1474
|
` + credentialGitReminder(ctx.projectDir) + (finalized2.warnings.length > 0 ? `
|
|
1359
1475
|
Warnings:
|
|
1360
|
-
${JSON.stringify(finalized2.warnings, null, 2)}` : "")
|
|
1476
|
+
${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
|
|
1477
|
+
{
|
|
1478
|
+
siteId: created.siteId,
|
|
1479
|
+
shortId: created.shortId,
|
|
1480
|
+
url: finalized2.url,
|
|
1481
|
+
deploymentId: created.deploymentId,
|
|
1482
|
+
mode: finalized2.mode,
|
|
1483
|
+
expiresAt: finalized2.expiresAt,
|
|
1484
|
+
filesUploaded: uploaded2,
|
|
1485
|
+
totalBytes: finalized2.totalBytes,
|
|
1486
|
+
warnings: finalized2.warnings,
|
|
1487
|
+
credentialStoredLocally: true
|
|
1488
|
+
}
|
|
1361
1489
|
);
|
|
1362
1490
|
}
|
|
1363
1491
|
const updateOnce = async (forceFullUpload) => {
|
|
@@ -1391,6 +1519,7 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : "")
|
|
|
1391
1519
|
const { uploaded, finalized } = update;
|
|
1392
1520
|
writeSiteFile(ctx.projectDir, { ...existing, url: finalized.url });
|
|
1393
1521
|
return text(
|
|
1522
|
+
"site_updated",
|
|
1394
1523
|
`Site updated: ${finalized.url}
|
|
1395
1524
|
Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
|
|
1396
1525
|
` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
|
|
@@ -1398,7 +1527,16 @@ Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
|
|
|
1398
1527
|
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.
|
|
1399
1528
|
` : "\nThis site is subscribed and permanent \u2014 no expiry.\n") + (finalized.warnings.length > 0 ? `
|
|
1400
1529
|
Warnings:
|
|
1401
|
-
${JSON.stringify(finalized.warnings, null, 2)}` : "")
|
|
1530
|
+
${JSON.stringify(finalized.warnings, null, 2)}` : ""),
|
|
1531
|
+
{
|
|
1532
|
+
siteId: existing.siteId,
|
|
1533
|
+
url: finalized.url,
|
|
1534
|
+
mode: finalized.mode,
|
|
1535
|
+
expiresAt: finalized.expiresAt,
|
|
1536
|
+
filesUploaded: uploaded,
|
|
1537
|
+
totalBytes: finalized.totalBytes,
|
|
1538
|
+
warnings: finalized.warnings
|
|
1539
|
+
}
|
|
1402
1540
|
);
|
|
1403
1541
|
} catch (e) {
|
|
1404
1542
|
return toolError(e);
|
|
@@ -1409,6 +1547,8 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : "")
|
|
|
1409
1547
|
"refresh_site",
|
|
1410
1548
|
{
|
|
1411
1549
|
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.",
|
|
1550
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1551
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1412
1552
|
inputSchema: {}
|
|
1413
1553
|
},
|
|
1414
1554
|
async () => {
|
|
@@ -1416,8 +1556,10 @@ ${JSON.stringify(finalized.warnings, null, 2)}` : "")
|
|
|
1416
1556
|
const site = requireSiteFile(ctx);
|
|
1417
1557
|
const res = await ctx.client.refreshSite(site.siteId, site.credential);
|
|
1418
1558
|
return text(
|
|
1559
|
+
"site_refreshed",
|
|
1419
1560
|
`Site validity refreshed. New expiry: ${res.expiresAt}
|
|
1420
|
-
Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refresh
|
|
1561
|
+
Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refresh.`,
|
|
1562
|
+
{ siteId: site.siteId, expiresAt: res.expiresAt }
|
|
1421
1563
|
);
|
|
1422
1564
|
} catch (e) {
|
|
1423
1565
|
return toolError(e);
|
|
@@ -1428,13 +1570,15 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1428
1570
|
"site_status",
|
|
1429
1571
|
{
|
|
1430
1572
|
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.",
|
|
1573
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1574
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
1431
1575
|
inputSchema: {}
|
|
1432
1576
|
},
|
|
1433
1577
|
async () => {
|
|
1434
1578
|
try {
|
|
1435
1579
|
const site = requireSiteFile(ctx);
|
|
1436
1580
|
const res = await ctx.client.getSiteStatus(site.siteId, site.credential);
|
|
1437
|
-
return textJson("Site status:", res);
|
|
1581
|
+
return textJson("site_status_returned", "Site status:", res);
|
|
1438
1582
|
} catch (e) {
|
|
1439
1583
|
return toolError(e);
|
|
1440
1584
|
}
|
|
@@ -1443,40 +1587,42 @@ Free sites stay live for ${FREE_SITE_TTL_HOURS} hours after each deploy or refre
|
|
|
1443
1587
|
server.registerTool(
|
|
1444
1588
|
"subscribe_site",
|
|
1445
1589
|
{
|
|
1446
|
-
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
|
|
1590
|
+
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 shows an over-limit notice and never changes billing automatically. The owner can explicitly choose another plan through Stripe Customer Portal. Card details are entered only on the Stripe-hosted page \u2014 never through the AI tool. Opening and completing Stripe Checkout is the final subscription confirmation.`,
|
|
1591
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1592
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1447
1593
|
inputSchema: {
|
|
1448
1594
|
plan: planEnum.describe(
|
|
1449
1595
|
"Monthly plan: water (very light personal pages), personal (personal brand / small shop), share (small-business site), business (steadier traffic, more headroom)."
|
|
1450
|
-
)
|
|
1451
|
-
confirm: z.boolean().optional().describe("User read the subscription disclosure and confirmed. Required to proceed.")
|
|
1596
|
+
)
|
|
1452
1597
|
}
|
|
1453
1598
|
},
|
|
1454
1599
|
async (args) => {
|
|
1455
1600
|
try {
|
|
1456
1601
|
const site = requireSiteFile(ctx);
|
|
1457
|
-
const siteUrl = site.url ?? (site.shortId ? `https://${site.shortId}.sakupa.com` : site.siteId);
|
|
1458
|
-
if (args.confirm !== true) {
|
|
1459
|
-
return text(
|
|
1460
|
-
`${subscriptionWarning(siteUrl, args.plan)}
|
|
1461
|
-
|
|
1462
|
-
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.`
|
|
1463
|
-
);
|
|
1464
|
-
}
|
|
1465
1602
|
const res = await ctx.client.createPlanCheckout(
|
|
1466
1603
|
{
|
|
1467
1604
|
siteId: site.siteId,
|
|
1468
1605
|
plan: args.plan,
|
|
1469
|
-
idempotencyKey: randomUUID()
|
|
1470
|
-
confirmPlan: true
|
|
1606
|
+
idempotencyKey: randomUUID()
|
|
1471
1607
|
},
|
|
1472
1608
|
site.credential
|
|
1473
1609
|
);
|
|
1474
1610
|
return text(
|
|
1611
|
+
"subscription_checkout_ready",
|
|
1475
1612
|
`Stripe Checkout link \u2014 Sakupa Hosting for this site: ${res.plan} plan, \xA5${res.monthlyPriceJpy}/month
|
|
1476
1613
|
${res.checkoutUrl}
|
|
1477
1614
|
|
|
1478
1615
|
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.
|
|
1479
|
-
Once payment confirms, the site becomes permanent on its current URL. Binding a custom domain (bind_domain) is optional and still requires DNS verification
|
|
1616
|
+
Once payment confirms, the site becomes permanent on its current URL. Binding a custom domain (bind_domain) is optional and still requires DNS verification.`,
|
|
1617
|
+
{
|
|
1618
|
+
siteId: res.siteId,
|
|
1619
|
+
plan: res.plan,
|
|
1620
|
+
monthlyPriceJpy: res.monthlyPriceJpy,
|
|
1621
|
+
checkoutUrl: res.checkoutUrl,
|
|
1622
|
+
sessionId: res.sessionId,
|
|
1623
|
+
finalConfirmationProvider: "stripe"
|
|
1624
|
+
},
|
|
1625
|
+
"waiting_user"
|
|
1480
1626
|
);
|
|
1481
1627
|
} catch (e) {
|
|
1482
1628
|
return toolError(e);
|
|
@@ -1487,36 +1633,54 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
|
|
|
1487
1633
|
"bind_domain",
|
|
1488
1634
|
{
|
|
1489
1635
|
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. The binding unit is the APEX domain: binding example.com automatically includes www.example.com (both serve the same content, one apex TXT verification covers both), and one site binds at most ONE apex domain \u2014 a second domain needs a second subscribed site. Requires an ACTIVE subscription (subscribe_site). Ownership is proven ONLY by DNS control of the apex \u2014 payment never grants ownership. A binding request never reserves the domain: whoever proves DNS control first gets it, and unverified requests expire after 72 hours. Call again with verificationId to check progress.",
|
|
1636
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1637
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1490
1638
|
inputSchema: {
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
)
|
|
1494
|
-
verificationId: z.string().optional().describe("Check an existing DNS verification instead of starting a new one.")
|
|
1639
|
+
action: z2.enum(["start", "status"]),
|
|
1640
|
+
hostname: z2.string().optional().describe("Required for start."),
|
|
1641
|
+
verificationId: z2.string().optional().describe("Required for status.")
|
|
1495
1642
|
}
|
|
1496
1643
|
},
|
|
1497
1644
|
async (args) => {
|
|
1498
1645
|
try {
|
|
1499
1646
|
const site = requireSiteFile(ctx);
|
|
1500
|
-
if (args.
|
|
1647
|
+
if (args.action === "status") {
|
|
1648
|
+
if (!args.verificationId) {
|
|
1649
|
+
throw new SakupaError("invalid_request", "verificationId is required for status");
|
|
1650
|
+
}
|
|
1501
1651
|
const res2 = await ctx.client.checkVerification(args.verificationId, site.credential);
|
|
1502
1652
|
if (res2.status === "verified") {
|
|
1503
1653
|
writeSiteFile(ctx.projectDir, { ...site, boundDomain: res2.apexDomain });
|
|
1504
1654
|
}
|
|
1505
1655
|
return text(
|
|
1656
|
+
res2.status === "verified" ? "domain_verification_succeeded" : "domain_verification_pending",
|
|
1506
1657
|
`DNS verification ${res2.verificationId}: ${res2.status}
|
|
1507
1658
|
${res2.message}
|
|
1508
1659
|
` + (res2.provisioningJobId ? `Provisioning started (job ${res2.provisioningJobId}). HTTPS certificates and serving setup are in progress; check again with bind_domain + verificationId later.
|
|
1509
1660
|
` : "") + (res2.pendingDnsRecords.length > 0 ? `
|
|
1510
1661
|
DNS records still required:
|
|
1511
|
-
${JSON.stringify(res2.pendingDnsRecords, null, 2)}` : "")
|
|
1662
|
+
${JSON.stringify(res2.pendingDnsRecords, null, 2)}` : ""),
|
|
1663
|
+
{
|
|
1664
|
+
verificationId: res2.verificationId,
|
|
1665
|
+
status: res2.status,
|
|
1666
|
+
apexDomain: res2.apexDomain,
|
|
1667
|
+
provisioningJobId: res2.provisioningJobId,
|
|
1668
|
+
pendingDnsRecords: res2.pendingDnsRecords,
|
|
1669
|
+
message: res2.message
|
|
1670
|
+
},
|
|
1671
|
+
res2.status === "verified" ? "completed" : "pending_provider"
|
|
1512
1672
|
);
|
|
1513
1673
|
}
|
|
1674
|
+
if (!args.hostname) {
|
|
1675
|
+
throw new SakupaError("invalid_request", "hostname is required for start");
|
|
1676
|
+
}
|
|
1514
1677
|
const req = {
|
|
1515
1678
|
siteId: site.siteId,
|
|
1516
1679
|
hostname: args.hostname
|
|
1517
1680
|
};
|
|
1518
1681
|
const res = await ctx.client.bindDomain(site.credential, req);
|
|
1519
1682
|
return text(
|
|
1683
|
+
"domain_verification_started",
|
|
1520
1684
|
`Domain binding started for ${res.apexDomain} (includes: ${res.includedHostnames.join(", ")} \u2014 both will serve this site).
|
|
1521
1685
|
|
|
1522
1686
|
1. Prove control of ${res.apexDomain} by creating this DNS record:
|
|
@@ -1527,7 +1691,15 @@ ${JSON.stringify(res2.pendingDnsRecords, null, 2)}` : "")
|
|
|
1527
1691
|
|
|
1528
1692
|
2. Serving DNS (after verification): ${res.servingInstructions}
|
|
1529
1693
|
|
|
1530
|
-
Then run bind_domain again with verificationId: "${res.verificationId}" to check verification and start provisioning
|
|
1694
|
+
Then run bind_domain again with verificationId: "${res.verificationId}" to check verification and start provisioning.`,
|
|
1695
|
+
{
|
|
1696
|
+
verificationId: res.verificationId,
|
|
1697
|
+
apexDomain: res.apexDomain,
|
|
1698
|
+
includedHostnames: res.includedHostnames,
|
|
1699
|
+
verificationRecord: res.verificationRecord,
|
|
1700
|
+
servingInstructions: res.servingInstructions
|
|
1701
|
+
},
|
|
1702
|
+
"waiting_user"
|
|
1531
1703
|
);
|
|
1532
1704
|
} catch (e) {
|
|
1533
1705
|
return toolError(e);
|
|
@@ -1538,6 +1710,8 @@ Then run bind_domain again with verificationId: "${res.verificationId}" to check
|
|
|
1538
1710
|
"billing_status",
|
|
1539
1711
|
{
|
|
1540
1712
|
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).",
|
|
1713
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1714
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
1541
1715
|
inputSchema: {}
|
|
1542
1716
|
},
|
|
1543
1717
|
async () => {
|
|
@@ -1556,7 +1730,7 @@ Then run bind_domain again with verificationId: "${res.verificationId}" to check
|
|
|
1556
1730
|
res.boundHostnames.length > 0 ? `Bound custom domains: ${res.boundHostnames.join(", ")}` : void 0,
|
|
1557
1731
|
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
|
|
1558
1732
|
].filter((l) => l !== void 0);
|
|
1559
|
-
return textJson(`${lines.join("\n")}
|
|
1733
|
+
return textJson("billing_status_returned", `${lines.join("\n")}
|
|
1560
1734
|
|
|
1561
1735
|
Full status:`, res);
|
|
1562
1736
|
} catch (e) {
|
|
@@ -1567,41 +1741,53 @@ Full status:`, res);
|
|
|
1567
1741
|
server.registerTool(
|
|
1568
1742
|
"manage_billing",
|
|
1569
1743
|
{
|
|
1570
|
-
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. With .sakupa/site.json, this opens the site-specific portal.
|
|
1744
|
+
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. With .sakupa/site.json, this opens the site-specific portal. Without the local credential, this returns Stripe's public no-code Customer Portal login page. The customer enters the checkout email and confirms a one-time passcode sent by Stripe. This never restores Sakupa site authority.",
|
|
1745
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1746
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1571
1747
|
inputSchema: {
|
|
1572
|
-
|
|
1573
|
-
"Without .sakupa/site.json only: the remembered https://{shortId}.sakupa.com URL."
|
|
1574
|
-
)
|
|
1748
|
+
scope: z2.enum(["site", "public_recovery"])
|
|
1575
1749
|
}
|
|
1576
1750
|
},
|
|
1577
1751
|
async (args) => {
|
|
1578
1752
|
try {
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
if (args.siteUrl !== void 0) {
|
|
1582
|
-
return text(
|
|
1583
|
-
"A local Sakupa credential is present, so manage_billing uses the site-specific Stripe portal. Remove siteUrl and call manage_billing again; the public lost-key path is deliberately unavailable while site authority is present."
|
|
1584
|
-
);
|
|
1585
|
-
}
|
|
1753
|
+
if (args.scope === "site") {
|
|
1754
|
+
const site = requireSiteFile(ctx);
|
|
1586
1755
|
const res2 = await ctx.client.createBillingPortal(site.siteId, site.credential);
|
|
1587
|
-
return
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1756
|
+
return structuredToolResult({
|
|
1757
|
+
schemaVersion: 1,
|
|
1758
|
+
outcome: "waiting_user",
|
|
1759
|
+
resultCode: "site_billing_portal_ready",
|
|
1760
|
+
summary: `\u5DF2\u521B\u5EFA\u6B64\u7AD9\u70B9\u7684 Stripe \u5BA2\u6237\u95E8\u6237\u77ED\u65F6\u94FE\u63A5\uFF1A${res2.portalUrl}\u3002\u4EFB\u4F55\u53D8\u66F4\u4ECD\u987B\u5728 Stripe \u9875\u9762\u5B8C\u6210\u3002`,
|
|
1761
|
+
data: { scope: args.scope, portalUrl: res2.portalUrl },
|
|
1762
|
+
userAction: {
|
|
1763
|
+
type: "open_url",
|
|
1764
|
+
provider: "stripe",
|
|
1765
|
+
url: res2.portalUrl,
|
|
1766
|
+
expectedOutcome: "\u7528\u6237\u5728 Stripe \u6258\u7BA1\u9875\u9762\u7BA1\u7406\u4ED8\u6B3E\u65B9\u5F0F\u3001\u53D1\u7968\u6216\u53D6\u6D88\u7EED\u8BA2"
|
|
1767
|
+
},
|
|
1768
|
+
nextActions: [{ tool: "billing_status", allowed: true }]
|
|
1769
|
+
});
|
|
1598
1770
|
}
|
|
1599
|
-
const res = await ctx.client.
|
|
1600
|
-
return
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1771
|
+
const res = await ctx.client.getPublicBillingPortal();
|
|
1772
|
+
return structuredToolResult({
|
|
1773
|
+
schemaVersion: 1,
|
|
1774
|
+
outcome: "waiting_user",
|
|
1775
|
+
resultCode: "public_billing_recovery_portal_ready",
|
|
1776
|
+
summary: `Stripe \u516C\u5171\u90AE\u7BB1 OTP \u767B\u5F55\u9875\uFF1A${res.portalUrl}\u3002\u5B83\u4F7F\u7528 one-time passcode\uFF0Cdoes not recover the Sakupa key\uFF0C\u4E5F\u4E0D\u6388\u4E88\u7AD9\u70B9\u6743\u9650\uFF1B\u540C\u90AE\u7BB1\u5B58\u5728\u591A\u4E2A Customer \u65F6\uFF0CStripe \u53EF\u80FD\u53EA\u6253\u5F00 most recently created \u7684\u53EF\u7528\u8BB0\u5F55\u3002`,
|
|
1777
|
+
data: {
|
|
1778
|
+
scope: args.scope,
|
|
1779
|
+
portalUrl: res.portalUrl,
|
|
1780
|
+
grantsSiteAuthority: false,
|
|
1781
|
+
acceptsSiteIdentifier: false
|
|
1782
|
+
},
|
|
1783
|
+
userAction: {
|
|
1784
|
+
type: "open_url",
|
|
1785
|
+
provider: "stripe",
|
|
1786
|
+
url: res.portalUrl,
|
|
1787
|
+
expectedOutcome: "\u7528\u6237\u7531 Stripe \u9A8C\u8BC1\u8D26\u5355\u90AE\u7BB1\u540E\u67E5\u770B\u5E76\u53D6\u6D88\u95E8\u6237\u4E2D\u663E\u793A\u7684\u8BA2\u9605"
|
|
1788
|
+
},
|
|
1789
|
+
nextActions: []
|
|
1790
|
+
});
|
|
1605
1791
|
} catch (e) {
|
|
1606
1792
|
return toolError(e);
|
|
1607
1793
|
}
|
|
@@ -1611,17 +1797,24 @@ For privacy, this response is identical whether or not the URL, site, customer,
|
|
|
1611
1797
|
"recover_domain_site",
|
|
1612
1798
|
{
|
|
1613
1799
|
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).",
|
|
1800
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1801
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
1614
1802
|
inputSchema: {
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1803
|
+
action: z2.enum(["start", "status", "complete"]),
|
|
1804
|
+
hostname: z2.string().optional().describe("Required for start."),
|
|
1805
|
+
verificationId: z2.string().optional().describe("Required for status or complete."),
|
|
1806
|
+
preserveExistingCredentials: z2.boolean().optional().describe("Explicitly keep old local credentials working (default: revoke them all).")
|
|
1618
1807
|
}
|
|
1619
1808
|
},
|
|
1620
1809
|
async (args) => {
|
|
1621
1810
|
try {
|
|
1622
|
-
if (args.
|
|
1811
|
+
if (args.action === "start") {
|
|
1812
|
+
if (!args.hostname) {
|
|
1813
|
+
throw new SakupaError("invalid_request", "hostname is required for start");
|
|
1814
|
+
}
|
|
1623
1815
|
const res2 = await ctx.client.recoverDomain({ hostname: args.hostname });
|
|
1624
1816
|
return text(
|
|
1817
|
+
"domain_recovery_started",
|
|
1625
1818
|
`Recovery started for ${args.hostname} (apex domain: ${res2.apexDomain}).
|
|
1626
1819
|
|
|
1627
1820
|
Create this DNS record to prove apex-domain control:
|
|
@@ -1633,9 +1826,40 @@ ${res2.message}
|
|
|
1633
1826
|
|
|
1634
1827
|
IMPORTANT: completing recovery revokes ALL previous local authorizations for this site by default (this protects you if the old project or its credential leaked). If you want to keep the old credentials working, pass preserveExistingCredentials: true when completing.
|
|
1635
1828
|
|
|
1636
|
-
After the DNS record resolves, re-run recover_domain_site with verificationId: "${res2.verificationId}"
|
|
1829
|
+
After the DNS record resolves, re-run recover_domain_site with verificationId: "${res2.verificationId}".`,
|
|
1830
|
+
{
|
|
1831
|
+
verificationId: res2.verificationId,
|
|
1832
|
+
apexDomain: res2.apexDomain,
|
|
1833
|
+
verificationRecord: res2.verificationRecord,
|
|
1834
|
+
revokesPreviousCredentialsByDefault: true
|
|
1835
|
+
},
|
|
1836
|
+
"waiting_user"
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
if (!args.verificationId) {
|
|
1840
|
+
throw new SakupaError(
|
|
1841
|
+
"invalid_request",
|
|
1842
|
+
"verificationId is required for status or complete"
|
|
1637
1843
|
);
|
|
1638
1844
|
}
|
|
1845
|
+
if (args.action === "status") {
|
|
1846
|
+
const res2 = await ctx.client.getRecoveryStatus(args.verificationId);
|
|
1847
|
+
return structuredToolResult({
|
|
1848
|
+
schemaVersion: 1,
|
|
1849
|
+
outcome: res2.status === "expired" ? "expired" : res2.readyToComplete ? "completed" : "pending_provider",
|
|
1850
|
+
resultCode: res2.status === "expired" ? "domain_recovery_expired" : res2.readyToComplete ? "domain_recovery_ready" : "domain_recovery_pending_dns",
|
|
1851
|
+
summary: `DNS \u6062\u590D\u9A8C\u8BC1\u72B6\u6001\uFF1A${res2.status}`,
|
|
1852
|
+
data: { recovery: res2 },
|
|
1853
|
+
nextActions: [
|
|
1854
|
+
{
|
|
1855
|
+
tool: "recover_domain_site",
|
|
1856
|
+
arguments: { action: "complete", verificationId: args.verificationId },
|
|
1857
|
+
allowed: res2.readyToComplete,
|
|
1858
|
+
...res2.readyToComplete ? {} : { reasonCode: res2.status }
|
|
1859
|
+
}
|
|
1860
|
+
]
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1639
1863
|
const res = await ctx.client.completeRecovery(args.verificationId, {
|
|
1640
1864
|
...args.preserveExistingCredentials !== void 0 ? { preserveExistingCredentials: args.preserveExistingCredentials } : {}
|
|
1641
1865
|
});
|
|
@@ -1647,6 +1871,7 @@ After the DNS record resolves, re-run recover_domain_site with verificationId: "
|
|
|
1647
1871
|
apiBaseUrl: ctx.apiBaseUrl
|
|
1648
1872
|
});
|
|
1649
1873
|
return text(
|
|
1874
|
+
"domain_recovery_completed",
|
|
1650
1875
|
`Recovery complete.
|
|
1651
1876
|
Site: ${res.siteId} (hostnames: ${res.boundHostnames.join(", ") || "(none)"})
|
|
1652
1877
|
Previous credentials revoked: ${res.revokedPreviousCredentials ? "YES" : "no (preserved on request)"}
|
|
@@ -1654,54 +1879,16 @@ Previous credentials revoked: ${res.revokedPreviousCredentials ? "YES" : "no (pr
|
|
|
1654
1879
|
A NEW management credential was written to .sakupa/site.json in this project \u2014 this project now manages the site.
|
|
1655
1880
|
` + credentialGitReminder(ctx.projectDir) + `
|
|
1656
1881
|
Download the current site content (signed URL):
|
|
1657
|
-
${res.archiveUrl}
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
"set_billing_plan",
|
|
1666
|
-
{
|
|
1667
|
-
description: `Change this site's hosting plan or cancel/re-enable renewal. 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; the paid service remains available until then. The site reverts to a free 24h site only after the signed Stripe final-cancellation webhook arrives. The API returns the consequences first; explicit owner confirmation (confirm: true) is required before anything is applied.`,
|
|
1668
|
-
inputSchema: {
|
|
1669
|
-
plan: planEnum.optional().describe("Target monthly plan."),
|
|
1670
|
-
cancelRenewal: z.boolean().optional().describe(
|
|
1671
|
-
"true: cancel renewal (the site stays permanent to the end of the paid month, then reverts to free). false: re-enable renewal."
|
|
1672
|
-
),
|
|
1673
|
-
confirm: z.boolean().optional().describe("User saw the consequences and explicitly confirmed.")
|
|
1674
|
-
}
|
|
1675
|
-
},
|
|
1676
|
-
async (args) => {
|
|
1677
|
-
try {
|
|
1678
|
-
const site = requireSiteFile(ctx);
|
|
1679
|
-
const req = {
|
|
1680
|
-
...args.plan !== void 0 ? { plan: args.plan } : {},
|
|
1681
|
-
...args.cancelRenewal !== void 0 ? { cancelRenewal: args.cancelRenewal } : {},
|
|
1682
|
-
confirm: args.confirm === true
|
|
1683
|
-
};
|
|
1684
|
-
try {
|
|
1685
|
-
const res = await ctx.client.setBillingPlan(site.siteId, site.credential, req);
|
|
1686
|
-
return textJson(
|
|
1687
|
-
`Billing updated for site ${res.siteId} (mode: ${res.mode}).
|
|
1688
|
-
Consequences:
|
|
1689
|
-
${res.consequences.map((c) => `- ${c}`).join("\n")}
|
|
1690
|
-
Result:`,
|
|
1691
|
-
res
|
|
1692
|
-
);
|
|
1693
|
-
} catch (e) {
|
|
1694
|
-
if (isSakupaError(e) && e.code === "confirmation_required") {
|
|
1695
|
-
return text(
|
|
1696
|
-
`Confirmation required before changing the billing plan \u2014 nothing was applied.
|
|
1697
|
-
|
|
1698
|
-
${e.message}
|
|
1699
|
-
` + (e.details !== void 0 ? `${JSON.stringify(e.details, null, 2)}
|
|
1700
|
-
` : "") + "\nPlease show these consequences to the user and, after their explicit confirmation, re-run set_billing_plan with the same arguments plus confirm: true."
|
|
1701
|
-
);
|
|
1882
|
+
${res.archiveUrl}`,
|
|
1883
|
+
{
|
|
1884
|
+
siteId: res.siteId,
|
|
1885
|
+
boundHostnames: res.boundHostnames,
|
|
1886
|
+
revokedPreviousCredentials: res.revokedPreviousCredentials,
|
|
1887
|
+
archiveUrl: res.archiveUrl,
|
|
1888
|
+
archiveExpiresAt: res.archiveExpiresAt,
|
|
1889
|
+
credentialStoredLocally: true
|
|
1702
1890
|
}
|
|
1703
|
-
|
|
1704
|
-
}
|
|
1891
|
+
);
|
|
1705
1892
|
} catch (e) {
|
|
1706
1893
|
return toolError(e);
|
|
1707
1894
|
}
|
|
@@ -1711,11 +1898,13 @@ ${e.message}
|
|
|
1711
1898
|
"create_support_ticket",
|
|
1712
1899
|
{
|
|
1713
1900
|
description: "Create a Sakupa support ticket for billing, payment, refund review, domain verification, deployment, serving or other issues the MCP cannot solve automatically. Do not include secrets, credentials or card data in the description.",
|
|
1901
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1902
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1714
1903
|
inputSchema: {
|
|
1715
1904
|
category: ticketCategoryEnum,
|
|
1716
|
-
subject:
|
|
1717
|
-
description:
|
|
1718
|
-
contactEmail:
|
|
1905
|
+
subject: z2.string().describe("Short subject line."),
|
|
1906
|
+
description: z2.string().describe("Problem description (no secrets, no card data)."),
|
|
1907
|
+
contactEmail: z2.string().optional().describe("Optional contact email for follow-up.")
|
|
1719
1908
|
}
|
|
1720
1909
|
},
|
|
1721
1910
|
async (args) => {
|
|
@@ -1728,7 +1917,11 @@ ${e.message}
|
|
|
1728
1917
|
description: args.description,
|
|
1729
1918
|
...args.contactEmail !== void 0 ? { contactEmail: args.contactEmail } : {}
|
|
1730
1919
|
});
|
|
1731
|
-
return text(
|
|
1920
|
+
return text(
|
|
1921
|
+
"support_ticket_created",
|
|
1922
|
+
`Support ticket created: ${res.ticketId} (status: ${res.status}).`,
|
|
1923
|
+
{ ticketId: res.ticketId, status: res.status }
|
|
1924
|
+
);
|
|
1732
1925
|
} catch (e) {
|
|
1733
1926
|
return toolError(e);
|
|
1734
1927
|
}
|
|
@@ -1738,15 +1931,17 @@ ${e.message}
|
|
|
1738
1931
|
"report_bug",
|
|
1739
1932
|
{
|
|
1740
1933
|
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.",
|
|
1934
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
1935
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
1741
1936
|
inputSchema: {
|
|
1742
|
-
toolName:
|
|
1743
|
-
errorCode:
|
|
1744
|
-
errorMessage:
|
|
1745
|
-
requestId:
|
|
1746
|
-
deploymentId:
|
|
1937
|
+
toolName: z2.string().describe('The Sakupa tool that failed, e.g. "deploy_site".'),
|
|
1938
|
+
errorCode: z2.string().optional(),
|
|
1939
|
+
errorMessage: z2.string().optional().describe("Sanitized error message (no secrets)."),
|
|
1940
|
+
requestId: z2.string().optional(),
|
|
1941
|
+
deploymentId: z2.string().optional(),
|
|
1747
1942
|
severity: severityEnum.optional(),
|
|
1748
|
-
description:
|
|
1749
|
-
confirmSubmit:
|
|
1943
|
+
description: z2.string().optional().describe("What happened, in the user's words (no secrets)."),
|
|
1944
|
+
confirmSubmit: z2.boolean().optional().describe("User reviewed the report payload and approved submission.")
|
|
1750
1945
|
}
|
|
1751
1946
|
},
|
|
1752
1947
|
async (args) => {
|
|
@@ -1772,14 +1967,18 @@ ${e.message}
|
|
|
1772
1967
|
};
|
|
1773
1968
|
if (args.confirmSubmit !== true) {
|
|
1774
1969
|
return textJson(
|
|
1970
|
+
"bug_report_preview_ready",
|
|
1775
1971
|
"Bug report prepared but NOT submitted. This is the exact payload that would be sent (structured diagnostics only \u2014 no file contents, source code or secrets). Please show it to the user; re-run report_bug with confirmSubmit: true to submit.",
|
|
1776
|
-
payload
|
|
1972
|
+
payload,
|
|
1973
|
+
"preview"
|
|
1777
1974
|
);
|
|
1778
1975
|
}
|
|
1779
1976
|
const res = await ctx.client.reportBug(payload, site?.credential);
|
|
1780
1977
|
return text(
|
|
1978
|
+
"bug_report_submitted",
|
|
1781
1979
|
`Bug report submitted. Ticket: ${res.ticketId}
|
|
1782
|
-
Summary: ${res.sanitizedSummary}
|
|
1980
|
+
Summary: ${res.sanitizedSummary}`,
|
|
1981
|
+
{ ticketId: res.ticketId, sanitizedSummary: res.sanitizedSummary }
|
|
1783
1982
|
);
|
|
1784
1983
|
} catch (e) {
|
|
1785
1984
|
return toolError(e);
|
|
@@ -1788,6 +1987,211 @@ Summary: ${res.sanitizedSummary}`
|
|
|
1788
1987
|
);
|
|
1789
1988
|
}
|
|
1790
1989
|
|
|
1990
|
+
// src/tools/billing.ts
|
|
1991
|
+
import { z as z3 } from "zod";
|
|
1992
|
+
var plan = z3.enum(["water", "personal", "share", "business"]);
|
|
1993
|
+
function registerBillingTools(server, ctx) {
|
|
1994
|
+
server.registerTool(
|
|
1995
|
+
"list_billing_plans",
|
|
1996
|
+
{
|
|
1997
|
+
description: "Return the authoritative Sakupa monthly plan catalog, exact limits, prices, catalog version and plan-change billing rules. This is read-only and does not require a site.",
|
|
1998
|
+
inputSchema: {},
|
|
1999
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2000
|
+
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true }
|
|
2001
|
+
},
|
|
2002
|
+
async () => {
|
|
2003
|
+
try {
|
|
2004
|
+
const catalog = await ctx.client.getBillingPlanCatalog();
|
|
2005
|
+
return structuredToolResult({
|
|
2006
|
+
schemaVersion: 1,
|
|
2007
|
+
outcome: "completed",
|
|
2008
|
+
resultCode: "billing_catalog_returned",
|
|
2009
|
+
summary: `\u5DF2\u8FD4\u56DE ${catalog.plans.length} \u4E2A\u6708\u4ED8\u65B9\u6848\uFF1BStripe \u6258\u7BA1\u9875\u9762\u662F\u4ED8\u8D39\u4E0E\u6539\u6863\u7684\u6700\u7EC8\u786E\u8BA4\u5165\u53E3\u3002`,
|
|
2010
|
+
data: { catalog },
|
|
2011
|
+
nextActions: [{ tool: "subscribe_site", allowed: true }]
|
|
2012
|
+
});
|
|
2013
|
+
} catch (error) {
|
|
2014
|
+
return toolError(error);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
);
|
|
2018
|
+
server.registerTool(
|
|
2019
|
+
"change_subscription_plan",
|
|
2020
|
+
{
|
|
2021
|
+
description: "Create a Stripe-hosted confirmation link for a manually selected subscription plan. Creating the link does not change billing; only the user can confirm on Stripe.",
|
|
2022
|
+
inputSchema: {
|
|
2023
|
+
targetPlan: plan,
|
|
2024
|
+
operationId: z3.string().min(1)
|
|
2025
|
+
},
|
|
2026
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2027
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
2028
|
+
},
|
|
2029
|
+
async (args) => {
|
|
2030
|
+
try {
|
|
2031
|
+
const site = requireSiteFile(ctx);
|
|
2032
|
+
const result = await ctx.client.changeSubscriptionPlan(site.credential, {
|
|
2033
|
+
siteId: site.siteId,
|
|
2034
|
+
targetPlan: args.targetPlan,
|
|
2035
|
+
operationId: args.operationId
|
|
2036
|
+
});
|
|
2037
|
+
return structuredToolResult({
|
|
2038
|
+
schemaVersion: 1,
|
|
2039
|
+
outcome: "waiting_user",
|
|
2040
|
+
resultCode: "stripe_plan_change_confirmation_required",
|
|
2041
|
+
operationId: args.operationId,
|
|
2042
|
+
summary: `\u5DF2\u751F\u6210\u4ECE ${result.currentPlan} \u5230 ${result.targetPlan} \u7684 Stripe \u786E\u8BA4\u94FE\u63A5\uFF1B\u8BA2\u9605\u5C1A\u672A\u53D8\u66F4\u3002`,
|
|
2043
|
+
data: { result },
|
|
2044
|
+
userAction: {
|
|
2045
|
+
type: "open_url",
|
|
2046
|
+
provider: "stripe",
|
|
2047
|
+
url: result.portalUrl,
|
|
2048
|
+
expectedOutcome: "\u7528\u6237\u5728 Stripe \u6258\u7BA1\u9875\u9762\u786E\u8BA4\u540E\uFF0C\u7531 webhook \u66F4\u65B0 Sakupa \u8BA2\u9605\u72B6\u6001"
|
|
2049
|
+
},
|
|
2050
|
+
nextActions: [{ tool: "billing_status", allowed: true }]
|
|
2051
|
+
});
|
|
2052
|
+
} catch (error) {
|
|
2053
|
+
return toolError(error);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// src/tools/lifecycle.ts
|
|
2060
|
+
import { z as z4 } from "zod";
|
|
2061
|
+
var deleteConfirmation = z4.object({
|
|
2062
|
+
siteId: z4.string().min(1),
|
|
2063
|
+
expectedSiteUpdatedAt: z4.string().datetime(),
|
|
2064
|
+
expectedStatus: z4.enum(["active", "expired", "deleted"]),
|
|
2065
|
+
expectedMode: z4.enum(["free", "paid"]),
|
|
2066
|
+
expectedServingMode: z4.enum(["normal", "over_limit_notice", "risk_notice", "stopped"]),
|
|
2067
|
+
expectedShortId: z4.string().optional(),
|
|
2068
|
+
expectedSubscriptionStatus: z4.enum(["incomplete", "active", "past_due", "canceled"]).optional(),
|
|
2069
|
+
expectedPlan: z4.enum(["water", "personal", "share", "business"]).optional(),
|
|
2070
|
+
expectedCancelAtPeriodEnd: z4.boolean().optional(),
|
|
2071
|
+
expectedCurrentPeriodEnd: z4.string().datetime().optional(),
|
|
2072
|
+
expectedLastDeploymentId: z4.string().optional(),
|
|
2073
|
+
expectedBoundHostnames: z4.array(z4.string()),
|
|
2074
|
+
acknowledge: z4.literal("delete_site_and_cancel_renewal")
|
|
2075
|
+
});
|
|
2076
|
+
var unbindConfirmation = z4.object({
|
|
2077
|
+
siteId: z4.string().min(1),
|
|
2078
|
+
bindingId: z4.string().min(1),
|
|
2079
|
+
expectedBindingUpdatedAt: z4.string().datetime(),
|
|
2080
|
+
expectedBindingStatus: z4.enum(["provisioning", "active"]),
|
|
2081
|
+
apexDomain: z4.string().min(1),
|
|
2082
|
+
expectedBoundHostnames: z4.array(z4.string()),
|
|
2083
|
+
acknowledge: z4.literal("unbind_domain_and_remove_custom_hostnames")
|
|
2084
|
+
});
|
|
2085
|
+
function registerLifecycleTools(server, ctx) {
|
|
2086
|
+
server.registerTool(
|
|
2087
|
+
"delete_site",
|
|
2088
|
+
{
|
|
2089
|
+
description: "Preview or execute deletion of this Sakupa site. Execution requires an exact server-validated confirmation bound to the current site state.",
|
|
2090
|
+
inputSchema: {
|
|
2091
|
+
action: z4.enum(["preview", "confirm"]),
|
|
2092
|
+
operationId: z4.string().min(1).optional(),
|
|
2093
|
+
confirmation: deleteConfirmation.optional()
|
|
2094
|
+
},
|
|
2095
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2096
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
2097
|
+
},
|
|
2098
|
+
async (args) => {
|
|
2099
|
+
try {
|
|
2100
|
+
const site = requireSiteFile(ctx);
|
|
2101
|
+
if (!args.operationId) {
|
|
2102
|
+
throw new Error("operationId is required for delete_site");
|
|
2103
|
+
}
|
|
2104
|
+
if (args.action === "preview") {
|
|
2105
|
+
const preview = await ctx.client.previewDeleteSite(site.siteId, site.credential, {
|
|
2106
|
+
operationId: args.operationId
|
|
2107
|
+
});
|
|
2108
|
+
return structuredToolResult({
|
|
2109
|
+
schemaVersion: 1,
|
|
2110
|
+
outcome: "waiting_user",
|
|
2111
|
+
resultCode: "delete_site_confirmation_required",
|
|
2112
|
+
operationId: args.operationId,
|
|
2113
|
+
summary: "\u5DF2\u8FD4\u56DE\u4E0E\u5F53\u524D\u7AD9\u70B9\u53CA\u8D26\u5355\u72B6\u6001\u7ED1\u5B9A\u7684\u5220\u9664\u540E\u679C\uFF1B\u786E\u8BA4\u540E\u5185\u5BB9\u548C\u6C38\u4E45\u5730\u5740\u4E0D\u53EF\u6062\u590D\u3002",
|
|
2114
|
+
data: { preview },
|
|
2115
|
+
nextActions: [
|
|
2116
|
+
{ tool: "delete_site", allowed: true, reasonCode: "exact_confirmation_required" }
|
|
2117
|
+
]
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
if (!args.confirmation) throw new Error("confirmation is required for confirm");
|
|
2121
|
+
const result = await ctx.client.deleteSite(site.siteId, site.credential, {
|
|
2122
|
+
operationId: args.operationId,
|
|
2123
|
+
confirmation: args.confirmation
|
|
2124
|
+
});
|
|
2125
|
+
deleteSiteFile(ctx.projectDir);
|
|
2126
|
+
return structuredToolResult({
|
|
2127
|
+
schemaVersion: 1,
|
|
2128
|
+
outcome: result.servingDeletionPending ? "pending_provider" : "completed",
|
|
2129
|
+
resultCode: "site_deleted",
|
|
2130
|
+
operationId: args.operationId,
|
|
2131
|
+
summary: "\u7AD9\u70B9\u5DF2\u5220\u9664\uFF0C\u672C\u5730\u7BA1\u7406\u51ED\u8BC1\u6587\u4EF6\u5DF2\u79FB\u9664\u3002",
|
|
2132
|
+
data: { result },
|
|
2133
|
+
nextActions: []
|
|
2134
|
+
});
|
|
2135
|
+
} catch (error) {
|
|
2136
|
+
return toolError(error);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
);
|
|
2140
|
+
server.registerTool(
|
|
2141
|
+
"unbind_domain",
|
|
2142
|
+
{
|
|
2143
|
+
description: "Preview or execute removal of the custom apex/www serving surface while preserving the subscription and permanent Sakupa URL.",
|
|
2144
|
+
inputSchema: {
|
|
2145
|
+
action: z4.enum(["preview", "confirm"]),
|
|
2146
|
+
operationId: z4.string().min(1).optional(),
|
|
2147
|
+
confirmation: unbindConfirmation.optional()
|
|
2148
|
+
},
|
|
2149
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2150
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
2151
|
+
},
|
|
2152
|
+
async (args) => {
|
|
2153
|
+
try {
|
|
2154
|
+
const site = requireSiteFile(ctx);
|
|
2155
|
+
if (!args.operationId) throw new Error("operationId is required for unbind_domain");
|
|
2156
|
+
if (args.action === "preview") {
|
|
2157
|
+
const preview = await ctx.client.previewUnbindDomain(site.siteId, site.credential, {
|
|
2158
|
+
operationId: args.operationId
|
|
2159
|
+
});
|
|
2160
|
+
return structuredToolResult({
|
|
2161
|
+
schemaVersion: 1,
|
|
2162
|
+
outcome: "waiting_user",
|
|
2163
|
+
resultCode: "unbind_domain_confirmation_required",
|
|
2164
|
+
operationId: args.operationId,
|
|
2165
|
+
summary: "\u5DF2\u8FD4\u56DE\u7CBE\u786E\u7ED1\u5B9A\u5FEB\u7167\uFF1B\u89E3\u7ED1\u53EA\u79FB\u9664\u81EA\u5B9A\u4E49\u57DF\u540D\uFF0C\u8BA2\u9605\u3001\u5185\u5BB9\u548C\u6C38\u4E45\u5730\u5740\u4FDD\u6301\u4E0D\u53D8\u3002",
|
|
2166
|
+
data: { preview },
|
|
2167
|
+
nextActions: [
|
|
2168
|
+
{ tool: "unbind_domain", allowed: true, reasonCode: "exact_confirmation_required" }
|
|
2169
|
+
]
|
|
2170
|
+
});
|
|
2171
|
+
}
|
|
2172
|
+
if (!args.confirmation) throw new Error("confirmation is required for confirm");
|
|
2173
|
+
const result = await ctx.client.unbindDomain(site.siteId, site.credential, {
|
|
2174
|
+
operationId: args.operationId,
|
|
2175
|
+
confirmation: args.confirmation
|
|
2176
|
+
});
|
|
2177
|
+
const { boundDomain: _removed, ...remaining } = site;
|
|
2178
|
+
writeSiteFile(ctx.projectDir, remaining);
|
|
2179
|
+
return structuredToolResult({
|
|
2180
|
+
schemaVersion: 1,
|
|
2181
|
+
outcome: result.servingDeletionPending ? "pending_provider" : "completed",
|
|
2182
|
+
resultCode: "domain_unbound",
|
|
2183
|
+
operationId: args.operationId,
|
|
2184
|
+
summary: "\u81EA\u5B9A\u4E49\u57DF\u540D\u5DF2\u89E3\u7ED1\uFF1B\u8BA2\u9605\u3001\u5DF2\u90E8\u7F72\u5185\u5BB9\u548C\u6C38\u4E45 Sakupa \u5730\u5740\u672A\u53D8\u3002",
|
|
2185
|
+
data: { result },
|
|
2186
|
+
nextActions: [{ tool: "site_status", allowed: true }]
|
|
2187
|
+
});
|
|
2188
|
+
} catch (error) {
|
|
2189
|
+
return toolError(error);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
);
|
|
2193
|
+
}
|
|
2194
|
+
|
|
1791
2195
|
// src/transport.ts
|
|
1792
2196
|
var FetchTransport = class {
|
|
1793
2197
|
baseUrl;
|
|
@@ -1853,14 +2257,17 @@ Workflow:
|
|
|
1853
2257
|
management credential in .sakupa/site.json. Deploying again updates the site and refreshes
|
|
1854
2258
|
its validity; refresh_site extends validity without uploading.
|
|
1855
2259
|
3. To make the site PERMANENT, subscribe it to a monthly hosting plan (subscribe_site ->
|
|
1856
|
-
Stripe-hosted checkout; water/personal/share/business
|
|
1857
|
-
|
|
2260
|
+
Stripe-hosted checkout; water/personal/share/business). Paying makes the
|
|
2261
|
+
{shortId}.sakupa.com URL permanent \u2014 that is what payment buys. Usage over the chosen plan
|
|
2262
|
+
shows an over-limit notice by default. An external AI may periodically query usage and
|
|
2263
|
+
recommend a plan, but Sakupa never changes a subscription automatically.
|
|
1858
2264
|
4. Optionally bind a custom domain to the subscribed site (bind_domain): an included extra
|
|
1859
2265
|
serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
|
|
1860
2266
|
first verified request wins; unverified requests expire after 72 hours. billing_status,
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
2267
|
+
billing_status, change_subscription_plan, manage_billing and recover_domain_site manage the paid
|
|
2268
|
+
lifecycle. Plan changes are confirmed only on Stripe and synchronized by Stripe webhook.
|
|
2269
|
+
A cancellation keeps the site paid through the current period. Sakupa reverts it to a free
|
|
2270
|
+
24h site and removes paid data after Stripe sends the signed final-cancellation webhook.
|
|
1864
2271
|
|
|
1865
2272
|
Safety boundaries:
|
|
1866
2273
|
- Static output only: no SSR, API routes, middleware, server actions, databases or online builds.
|
|
@@ -1868,9 +2275,9 @@ Safety boundaries:
|
|
|
1868
2275
|
- Payment card data is entered only on Stripe-hosted pages \u2014 never through the AI tool.
|
|
1869
2276
|
- A subscription never grants domain ownership; only DNS verification does.
|
|
1870
2277
|
- The management credential lives only in .sakupa/site.json; never share or upload it. Without
|
|
1871
|
-
a bound custom domain, a lost credential is unrecoverable by design. manage_billing
|
|
1872
|
-
|
|
1873
|
-
|
|
2278
|
+
a bound custom domain, a lost credential is unrecoverable by design. manage_billing then opens
|
|
2279
|
+
Stripe's public no-code portal login, where the customer verifies the checkout email with a
|
|
2280
|
+
Stripe one-time passcode; it never restores site authority.`;
|
|
1874
2281
|
function createSakupaMcpServer(opts) {
|
|
1875
2282
|
const client = opts.client ?? new HttpApiClient(new FetchTransport(opts.apiBaseUrl));
|
|
1876
2283
|
const server = new McpServer(
|
|
@@ -1882,6 +2289,16 @@ function createSakupaMcpServer(opts) {
|
|
|
1882
2289
|
projectDir: opts.projectDir,
|
|
1883
2290
|
apiBaseUrl: opts.apiBaseUrl
|
|
1884
2291
|
});
|
|
2292
|
+
registerBillingTools(server, {
|
|
2293
|
+
client,
|
|
2294
|
+
projectDir: opts.projectDir,
|
|
2295
|
+
apiBaseUrl: opts.apiBaseUrl
|
|
2296
|
+
});
|
|
2297
|
+
registerLifecycleTools(server, {
|
|
2298
|
+
client,
|
|
2299
|
+
projectDir: opts.projectDir,
|
|
2300
|
+
apiBaseUrl: opts.apiBaseUrl
|
|
2301
|
+
});
|
|
1885
2302
|
return server;
|
|
1886
2303
|
}
|
|
1887
2304
|
|