@sakupa/mcp 0.6.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 +217 -100
- package/dist/index.js +218 -106
- 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,18 +452,6 @@ async function sha256Hex(bytes) {
|
|
|
452
452
|
return hex;
|
|
453
453
|
}
|
|
454
454
|
|
|
455
|
-
// ../core/dist/domain/billing-operation.js
|
|
456
|
-
var BILLING_MUTATION_LIMITS = {
|
|
457
|
-
globalPerTenMinutes: 20,
|
|
458
|
-
sitePerTenMinutes: 1,
|
|
459
|
-
perBillingPeriod: 3,
|
|
460
|
-
maxConcurrency: 2,
|
|
461
|
-
leaseSeconds: 30,
|
|
462
|
-
maxConsecutiveFailures: 5,
|
|
463
|
-
circuitBreakSeconds: 15 * 60,
|
|
464
|
-
providerTimeoutMs: 1e4
|
|
465
|
-
};
|
|
466
|
-
|
|
467
455
|
// ../core/dist/dto.js
|
|
468
456
|
var CREDENTIAL_HEADER = "x-sakupa-credential";
|
|
469
457
|
var IDEMPOTENCY_HEADER = "x-sakupa-idempotency-key";
|
|
@@ -473,9 +461,6 @@ var MCP_VERSION_HEADER = "x-sakupa-mcp-version";
|
|
|
473
461
|
var utf8Decoder = new TextDecoder("utf-8", { fatal: false });
|
|
474
462
|
var utf8Encoder = new TextEncoder();
|
|
475
463
|
|
|
476
|
-
// ../core/dist/services/authorization.js
|
|
477
|
-
var AUTHORIZATION_TTL_MS = 15 * 60 * 1e3;
|
|
478
|
-
|
|
479
464
|
// ../core/dist/services/subscriptions.js
|
|
480
465
|
var WEBHOOK_PROCESSING_LEASE_MS = 5 * 60 * 1e3;
|
|
481
466
|
|
|
@@ -574,8 +559,18 @@ var HttpApiClient = class {
|
|
|
574
559
|
credential
|
|
575
560
|
});
|
|
576
561
|
}
|
|
577
|
-
async deleteSite(siteId, credential) {
|
|
578
|
-
|
|
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
|
+
);
|
|
579
574
|
}
|
|
580
575
|
async bindDomain(credential, req) {
|
|
581
576
|
return this.call("POST", "/v1/domains/bind", { credential, body: req });
|
|
@@ -587,6 +582,20 @@ var HttpApiClient = class {
|
|
|
587
582
|
{ credential }
|
|
588
583
|
);
|
|
589
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
|
+
}
|
|
590
599
|
async recoverDomain(req) {
|
|
591
600
|
return this.call("POST", "/v1/domains/recover", { body: req });
|
|
592
601
|
}
|
|
@@ -629,10 +638,10 @@ var HttpApiClient = class {
|
|
|
629
638
|
async getBillingPlanCatalog() {
|
|
630
639
|
return this.call("GET", "/v1/billing/plans");
|
|
631
640
|
}
|
|
632
|
-
async
|
|
641
|
+
async changeSubscriptionPlan(credential, req) {
|
|
633
642
|
return this.call(
|
|
634
643
|
"POST",
|
|
635
|
-
`/v1/sites/${encodeURIComponent(siteId)}/billing/
|
|
644
|
+
`/v1/sites/${encodeURIComponent(req.siteId)}/billing/plan-change`,
|
|
636
645
|
{ credential, body: req }
|
|
637
646
|
);
|
|
638
647
|
}
|
|
@@ -1130,6 +1139,12 @@ function writeSiteFile(projectDir, file) {
|
|
|
1130
1139
|
} catch {
|
|
1131
1140
|
}
|
|
1132
1141
|
}
|
|
1142
|
+
function deleteSiteFile(projectDir) {
|
|
1143
|
+
const path = siteFilePath(projectDir);
|
|
1144
|
+
if (existsSync(path)) {
|
|
1145
|
+
rmSync(path, { force: true });
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1133
1148
|
function isInsideGitRepo(projectDir) {
|
|
1134
1149
|
let dir = projectDir;
|
|
1135
1150
|
for (; ; ) {
|
|
@@ -1202,14 +1217,32 @@ function requireSiteFile(ctx) {
|
|
|
1202
1217
|
}
|
|
1203
1218
|
function toolError(e) {
|
|
1204
1219
|
const errorCode = isSakupaError(e) ? e.code : "internal";
|
|
1205
|
-
const retryable =
|
|
1206
|
-
const
|
|
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";
|
|
1207
1236
|
const result = structuredToolResult({
|
|
1208
1237
|
schemaVersion: 1,
|
|
1209
1238
|
outcome: "failed",
|
|
1210
1239
|
resultCode: `error_${errorCode}`,
|
|
1211
1240
|
summary: safeSummary,
|
|
1212
|
-
data: {
|
|
1241
|
+
data: {
|
|
1242
|
+
errorCode,
|
|
1243
|
+
retryable,
|
|
1244
|
+
...safeDetails && Object.keys(safeDetails).length > 0 ? { details: safeDetails } : {}
|
|
1245
|
+
},
|
|
1213
1246
|
nextActions: []
|
|
1214
1247
|
});
|
|
1215
1248
|
return { ...result, isError: true };
|
|
@@ -1957,7 +1990,6 @@ Summary: ${res.sanitizedSummary}`,
|
|
|
1957
1990
|
// src/tools/billing.ts
|
|
1958
1991
|
import { z as z3 } from "zod";
|
|
1959
1992
|
var plan = z3.enum(["water", "personal", "share", "business"]);
|
|
1960
|
-
var trigger = z3.enum(["actual_overage", "credential_automation"]);
|
|
1961
1993
|
function registerBillingTools(server, ctx) {
|
|
1962
1994
|
server.registerTool(
|
|
1963
1995
|
"list_billing_plans",
|
|
@@ -1984,22 +2016,81 @@ function registerBillingTools(server, ctx) {
|
|
|
1984
2016
|
}
|
|
1985
2017
|
);
|
|
1986
2018
|
server.registerTool(
|
|
1987
|
-
"
|
|
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",
|
|
1988
2088
|
{
|
|
1989
|
-
description: "
|
|
2089
|
+
description: "Preview or execute deletion of this Sakupa site. Execution requires an exact server-validated confirmation bound to the current site state.",
|
|
1990
2090
|
inputSchema: {
|
|
1991
|
-
action:
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
"disable_automation",
|
|
1995
|
-
"request_authorized_upgrade"
|
|
1996
|
-
]),
|
|
1997
|
-
operationId: z3.string().min(1).describe("Stable idempotency key chosen by the caller."),
|
|
1998
|
-
targetPlan: plan.optional(),
|
|
1999
|
-
maxPlan: plan.optional(),
|
|
2000
|
-
allowedTriggers: z3.array(trigger).min(1).optional(),
|
|
2001
|
-
observedSnapshotId: z3.string().optional(),
|
|
2002
|
-
observedCatalogVersion: z3.string().optional()
|
|
2091
|
+
action: z4.enum(["preview", "confirm"]),
|
|
2092
|
+
operationId: z4.string().min(1).optional(),
|
|
2093
|
+
confirmation: deleteConfirmation.optional()
|
|
2003
2094
|
},
|
|
2004
2095
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2005
2096
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
@@ -2007,71 +2098,92 @@ function registerBillingTools(server, ctx) {
|
|
|
2007
2098
|
async (args) => {
|
|
2008
2099
|
try {
|
|
2009
2100
|
const site = requireSiteFile(ctx);
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
request = {
|
|
2016
|
-
action: args.action,
|
|
2017
|
-
targetPlan: args.targetPlan,
|
|
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, {
|
|
2018
2106
|
operationId: args.operationId
|
|
2019
|
-
};
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
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, {
|
|
2031
2158
|
operationId: args.operationId
|
|
2032
|
-
};
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
throw new SakupaError(
|
|
2038
|
-
"invalid_request",
|
|
2039
|
-
"targetPlan is required for request_authorized_upgrade"
|
|
2040
|
-
);
|
|
2041
|
-
}
|
|
2042
|
-
request = {
|
|
2043
|
-
action: args.action,
|
|
2159
|
+
});
|
|
2160
|
+
return structuredToolResult({
|
|
2161
|
+
schemaVersion: 1,
|
|
2162
|
+
outcome: "waiting_user",
|
|
2163
|
+
resultCode: "unbind_domain_confirmation_required",
|
|
2044
2164
|
operationId: args.operationId,
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
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
|
+
});
|
|
2050
2171
|
}
|
|
2051
|
-
|
|
2052
|
-
const
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
const
|
|
2057
|
-
|
|
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);
|
|
2058
2179
|
return structuredToolResult({
|
|
2059
2180
|
schemaVersion: 1,
|
|
2060
|
-
outcome,
|
|
2061
|
-
resultCode,
|
|
2181
|
+
outcome: result.servingDeletionPending ? "pending_provider" : "completed",
|
|
2182
|
+
resultCode: "domain_unbound",
|
|
2062
2183
|
operationId: args.operationId,
|
|
2063
|
-
summary:
|
|
2064
|
-
data: {
|
|
2065
|
-
|
|
2066
|
-
userAction: {
|
|
2067
|
-
type: "open_url",
|
|
2068
|
-
provider: portalUrl ? "stripe" : "sakupa",
|
|
2069
|
-
url,
|
|
2070
|
-
..."expiresAt" in result && typeof result.expiresAt === "string" ? { expiresAt: result.expiresAt } : {},
|
|
2071
|
-
expectedOutcome: portalUrl ? "\u7528\u6237\u5728 Stripe \u6258\u7BA1\u9875\u9762\u786E\u8BA4\u540E\uFF0C\u7531 webhook \u66F4\u65B0 Sakupa \u72B6\u6001" : "\u7528\u6237\u5728 Sakupa \u77ED\u65F6\u6548\u9875\u9762\u786E\u8BA4\u7CBE\u786E\u81EA\u52A8\u5347\u7EA7\u6388\u6743"
|
|
2072
|
-
}
|
|
2073
|
-
} : {},
|
|
2074
|
-
nextActions: [{ tool: "billing_status", allowed: true }]
|
|
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 }]
|
|
2075
2187
|
});
|
|
2076
2188
|
} catch (error) {
|
|
2077
2189
|
return toolError(error);
|
|
@@ -2147,13 +2259,13 @@ Workflow:
|
|
|
2147
2259
|
3. To make the site PERMANENT, subscribe it to a monthly hosting plan (subscribe_site ->
|
|
2148
2260
|
Stripe-hosted checkout; water/personal/share/business). Paying makes the
|
|
2149
2261
|
{shortId}.sakupa.com URL permanent \u2014 that is what payment buys. Usage over the chosen plan
|
|
2150
|
-
shows an over-limit notice by default.
|
|
2151
|
-
|
|
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.
|
|
2152
2264
|
4. Optionally bind a custom domain to the subscribed site (bind_domain): an included extra
|
|
2153
2265
|
serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
|
|
2154
2266
|
first verified request wins; unverified requests expire after 72 hours. billing_status,
|
|
2155
|
-
|
|
2156
|
-
changes are confirmed only on Stripe
|
|
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.
|
|
2157
2269
|
A cancellation keeps the site paid through the current period. Sakupa reverts it to a free
|
|
2158
2270
|
24h site and removes paid data after Stripe sends the signed final-cancellation webhook.
|
|
2159
2271
|
|
|
@@ -2182,6 +2294,11 @@ function createSakupaMcpServer(opts) {
|
|
|
2182
2294
|
projectDir: opts.projectDir,
|
|
2183
2295
|
apiBaseUrl: opts.apiBaseUrl
|
|
2184
2296
|
});
|
|
2297
|
+
registerLifecycleTools(server, {
|
|
2298
|
+
client,
|
|
2299
|
+
projectDir: opts.projectDir,
|
|
2300
|
+
apiBaseUrl: opts.apiBaseUrl
|
|
2301
|
+
});
|
|
2185
2302
|
return server;
|
|
2186
2303
|
}
|
|
2187
2304
|
|
package/dist/index.js
CHANGED
|
@@ -121,7 +121,7 @@ var FORBIDDEN_PATH_SEGMENTS = [
|
|
|
121
121
|
var ALLOWED_HIDDEN_PATHS = [".well-known/"];
|
|
122
122
|
|
|
123
123
|
// ../core/dist/domain/version.js
|
|
124
|
-
var SAKUPA_MCP_VERSION = "0.
|
|
124
|
+
var SAKUPA_MCP_VERSION = "0.7.0";
|
|
125
125
|
|
|
126
126
|
// ../core/dist/domain/errors.js
|
|
127
127
|
var HTTP_STATUS = {
|
|
@@ -446,18 +446,6 @@ async function sha256Hex(bytes) {
|
|
|
446
446
|
return hex;
|
|
447
447
|
}
|
|
448
448
|
|
|
449
|
-
// ../core/dist/domain/billing-operation.js
|
|
450
|
-
var BILLING_MUTATION_LIMITS = {
|
|
451
|
-
globalPerTenMinutes: 20,
|
|
452
|
-
sitePerTenMinutes: 1,
|
|
453
|
-
perBillingPeriod: 3,
|
|
454
|
-
maxConcurrency: 2,
|
|
455
|
-
leaseSeconds: 30,
|
|
456
|
-
maxConsecutiveFailures: 5,
|
|
457
|
-
circuitBreakSeconds: 15 * 60,
|
|
458
|
-
providerTimeoutMs: 1e4
|
|
459
|
-
};
|
|
460
|
-
|
|
461
449
|
// ../core/dist/dto.js
|
|
462
450
|
var CREDENTIAL_HEADER = "x-sakupa-credential";
|
|
463
451
|
var IDEMPOTENCY_HEADER = "x-sakupa-idempotency-key";
|
|
@@ -467,9 +455,6 @@ var MCP_VERSION_HEADER = "x-sakupa-mcp-version";
|
|
|
467
455
|
var utf8Decoder = new TextDecoder("utf-8", { fatal: false });
|
|
468
456
|
var utf8Encoder = new TextEncoder();
|
|
469
457
|
|
|
470
|
-
// ../core/dist/services/authorization.js
|
|
471
|
-
var AUTHORIZATION_TTL_MS = 15 * 60 * 1e3;
|
|
472
|
-
|
|
473
458
|
// ../core/dist/services/subscriptions.js
|
|
474
459
|
var WEBHOOK_PROCESSING_LEASE_MS = 5 * 60 * 1e3;
|
|
475
460
|
|
|
@@ -622,8 +607,18 @@ var HttpApiClient = class {
|
|
|
622
607
|
credential
|
|
623
608
|
});
|
|
624
609
|
}
|
|
625
|
-
async deleteSite(siteId, credential) {
|
|
626
|
-
|
|
610
|
+
async deleteSite(siteId, credential, req) {
|
|
611
|
+
return this.call("POST", `/v1/sites/${encodeURIComponent(siteId)}/delete`, {
|
|
612
|
+
credential,
|
|
613
|
+
body: req
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
async previewDeleteSite(siteId, credential, req) {
|
|
617
|
+
return this.call(
|
|
618
|
+
"POST",
|
|
619
|
+
`/v1/sites/${encodeURIComponent(siteId)}/delete/preview`,
|
|
620
|
+
{ credential, body: req }
|
|
621
|
+
);
|
|
627
622
|
}
|
|
628
623
|
async bindDomain(credential, req) {
|
|
629
624
|
return this.call("POST", "/v1/domains/bind", { credential, body: req });
|
|
@@ -635,6 +630,20 @@ var HttpApiClient = class {
|
|
|
635
630
|
{ credential }
|
|
636
631
|
);
|
|
637
632
|
}
|
|
633
|
+
async unbindDomain(siteId, credential, req) {
|
|
634
|
+
return this.call(
|
|
635
|
+
"POST",
|
|
636
|
+
`/v1/sites/${encodeURIComponent(siteId)}/domain/unbind`,
|
|
637
|
+
{ credential, body: req }
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
async previewUnbindDomain(siteId, credential, req) {
|
|
641
|
+
return this.call(
|
|
642
|
+
"POST",
|
|
643
|
+
`/v1/sites/${encodeURIComponent(siteId)}/domain/unbind/preview`,
|
|
644
|
+
{ credential, body: req }
|
|
645
|
+
);
|
|
646
|
+
}
|
|
638
647
|
async recoverDomain(req) {
|
|
639
648
|
return this.call("POST", "/v1/domains/recover", { body: req });
|
|
640
649
|
}
|
|
@@ -677,10 +686,10 @@ var HttpApiClient = class {
|
|
|
677
686
|
async getBillingPlanCatalog() {
|
|
678
687
|
return this.call("GET", "/v1/billing/plans");
|
|
679
688
|
}
|
|
680
|
-
async
|
|
689
|
+
async changeSubscriptionPlan(credential, req) {
|
|
681
690
|
return this.call(
|
|
682
691
|
"POST",
|
|
683
|
-
`/v1/sites/${encodeURIComponent(siteId)}/billing/
|
|
692
|
+
`/v1/sites/${encodeURIComponent(req.siteId)}/billing/plan-change`,
|
|
684
693
|
{ credential, body: req }
|
|
685
694
|
);
|
|
686
695
|
}
|
|
@@ -1246,14 +1255,32 @@ function requireSiteFile(ctx) {
|
|
|
1246
1255
|
}
|
|
1247
1256
|
function toolError(e) {
|
|
1248
1257
|
const errorCode = isSakupaError(e) ? e.code : "internal";
|
|
1249
|
-
const retryable =
|
|
1250
|
-
const
|
|
1258
|
+
const retryable = errorCode === "rate_limited" || errorCode === "internal";
|
|
1259
|
+
const safeDetailKeys = /* @__PURE__ */ new Set([
|
|
1260
|
+
"retryAfterSeconds",
|
|
1261
|
+
"reasonCode",
|
|
1262
|
+
"currentStatus",
|
|
1263
|
+
"expectedStatus",
|
|
1264
|
+
"minimumVersion",
|
|
1265
|
+
"currentVersion"
|
|
1266
|
+
]);
|
|
1267
|
+
const rawDetails = isSakupaError(e) && e.details && typeof e.details === "object" ? e.details : void 0;
|
|
1268
|
+
const safeDetails = rawDetails ? Object.fromEntries(
|
|
1269
|
+
Object.entries(rawDetails).filter(
|
|
1270
|
+
([key, value]) => safeDetailKeys.has(key) && (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
1271
|
+
)
|
|
1272
|
+
) : void 0;
|
|
1273
|
+
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";
|
|
1251
1274
|
const result = structuredToolResult({
|
|
1252
1275
|
schemaVersion: 1,
|
|
1253
1276
|
outcome: "failed",
|
|
1254
1277
|
resultCode: `error_${errorCode}`,
|
|
1255
1278
|
summary: safeSummary,
|
|
1256
|
-
data: {
|
|
1279
|
+
data: {
|
|
1280
|
+
errorCode,
|
|
1281
|
+
retryable,
|
|
1282
|
+
...safeDetails && Object.keys(safeDetails).length > 0 ? { details: safeDetails } : {}
|
|
1283
|
+
},
|
|
1257
1284
|
nextActions: []
|
|
1258
1285
|
});
|
|
1259
1286
|
return { ...result, isError: true };
|
|
@@ -2002,13 +2029,148 @@ Summary: ${res.sanitizedSummary}`,
|
|
|
2002
2029
|
);
|
|
2003
2030
|
}
|
|
2004
2031
|
|
|
2032
|
+
// src/tools/lifecycle.ts
|
|
2033
|
+
import { z as z3 } from "zod";
|
|
2034
|
+
var deleteConfirmation = z3.object({
|
|
2035
|
+
siteId: z3.string().min(1),
|
|
2036
|
+
expectedSiteUpdatedAt: z3.string().datetime(),
|
|
2037
|
+
expectedStatus: z3.enum(["active", "expired", "deleted"]),
|
|
2038
|
+
expectedMode: z3.enum(["free", "paid"]),
|
|
2039
|
+
expectedServingMode: z3.enum(["normal", "over_limit_notice", "risk_notice", "stopped"]),
|
|
2040
|
+
expectedShortId: z3.string().optional(),
|
|
2041
|
+
expectedSubscriptionStatus: z3.enum(["incomplete", "active", "past_due", "canceled"]).optional(),
|
|
2042
|
+
expectedPlan: z3.enum(["water", "personal", "share", "business"]).optional(),
|
|
2043
|
+
expectedCancelAtPeriodEnd: z3.boolean().optional(),
|
|
2044
|
+
expectedCurrentPeriodEnd: z3.string().datetime().optional(),
|
|
2045
|
+
expectedLastDeploymentId: z3.string().optional(),
|
|
2046
|
+
expectedBoundHostnames: z3.array(z3.string()),
|
|
2047
|
+
acknowledge: z3.literal("delete_site_and_cancel_renewal")
|
|
2048
|
+
});
|
|
2049
|
+
var unbindConfirmation = z3.object({
|
|
2050
|
+
siteId: z3.string().min(1),
|
|
2051
|
+
bindingId: z3.string().min(1),
|
|
2052
|
+
expectedBindingUpdatedAt: z3.string().datetime(),
|
|
2053
|
+
expectedBindingStatus: z3.enum(["provisioning", "active"]),
|
|
2054
|
+
apexDomain: z3.string().min(1),
|
|
2055
|
+
expectedBoundHostnames: z3.array(z3.string()),
|
|
2056
|
+
acknowledge: z3.literal("unbind_domain_and_remove_custom_hostnames")
|
|
2057
|
+
});
|
|
2058
|
+
function registerLifecycleTools(server, ctx) {
|
|
2059
|
+
server.registerTool(
|
|
2060
|
+
"delete_site",
|
|
2061
|
+
{
|
|
2062
|
+
description: "Preview or execute deletion of this Sakupa site. Execution requires an exact server-validated confirmation bound to the current site state.",
|
|
2063
|
+
inputSchema: {
|
|
2064
|
+
action: z3.enum(["preview", "confirm"]),
|
|
2065
|
+
operationId: z3.string().min(1).optional(),
|
|
2066
|
+
confirmation: deleteConfirmation.optional()
|
|
2067
|
+
},
|
|
2068
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2069
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
2070
|
+
},
|
|
2071
|
+
async (args) => {
|
|
2072
|
+
try {
|
|
2073
|
+
const site = requireSiteFile(ctx);
|
|
2074
|
+
if (!args.operationId) {
|
|
2075
|
+
throw new Error("operationId is required for delete_site");
|
|
2076
|
+
}
|
|
2077
|
+
if (args.action === "preview") {
|
|
2078
|
+
const preview = await ctx.client.previewDeleteSite(site.siteId, site.credential, {
|
|
2079
|
+
operationId: args.operationId
|
|
2080
|
+
});
|
|
2081
|
+
return structuredToolResult({
|
|
2082
|
+
schemaVersion: 1,
|
|
2083
|
+
outcome: "waiting_user",
|
|
2084
|
+
resultCode: "delete_site_confirmation_required",
|
|
2085
|
+
operationId: args.operationId,
|
|
2086
|
+
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",
|
|
2087
|
+
data: { preview },
|
|
2088
|
+
nextActions: [
|
|
2089
|
+
{ tool: "delete_site", allowed: true, reasonCode: "exact_confirmation_required" }
|
|
2090
|
+
]
|
|
2091
|
+
});
|
|
2092
|
+
}
|
|
2093
|
+
if (!args.confirmation) throw new Error("confirmation is required for confirm");
|
|
2094
|
+
const result = await ctx.client.deleteSite(site.siteId, site.credential, {
|
|
2095
|
+
operationId: args.operationId,
|
|
2096
|
+
confirmation: args.confirmation
|
|
2097
|
+
});
|
|
2098
|
+
deleteSiteFile(ctx.projectDir);
|
|
2099
|
+
return structuredToolResult({
|
|
2100
|
+
schemaVersion: 1,
|
|
2101
|
+
outcome: result.servingDeletionPending ? "pending_provider" : "completed",
|
|
2102
|
+
resultCode: "site_deleted",
|
|
2103
|
+
operationId: args.operationId,
|
|
2104
|
+
summary: "\u7AD9\u70B9\u5DF2\u5220\u9664\uFF0C\u672C\u5730\u7BA1\u7406\u51ED\u8BC1\u6587\u4EF6\u5DF2\u79FB\u9664\u3002",
|
|
2105
|
+
data: { result },
|
|
2106
|
+
nextActions: []
|
|
2107
|
+
});
|
|
2108
|
+
} catch (error) {
|
|
2109
|
+
return toolError(error);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
);
|
|
2113
|
+
server.registerTool(
|
|
2114
|
+
"unbind_domain",
|
|
2115
|
+
{
|
|
2116
|
+
description: "Preview or execute removal of the custom apex/www serving surface while preserving the subscription and permanent Sakupa URL.",
|
|
2117
|
+
inputSchema: {
|
|
2118
|
+
action: z3.enum(["preview", "confirm"]),
|
|
2119
|
+
operationId: z3.string().min(1).optional(),
|
|
2120
|
+
confirmation: unbindConfirmation.optional()
|
|
2121
|
+
},
|
|
2122
|
+
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2123
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
2124
|
+
},
|
|
2125
|
+
async (args) => {
|
|
2126
|
+
try {
|
|
2127
|
+
const site = requireSiteFile(ctx);
|
|
2128
|
+
if (!args.operationId) throw new Error("operationId is required for unbind_domain");
|
|
2129
|
+
if (args.action === "preview") {
|
|
2130
|
+
const preview = await ctx.client.previewUnbindDomain(site.siteId, site.credential, {
|
|
2131
|
+
operationId: args.operationId
|
|
2132
|
+
});
|
|
2133
|
+
return structuredToolResult({
|
|
2134
|
+
schemaVersion: 1,
|
|
2135
|
+
outcome: "waiting_user",
|
|
2136
|
+
resultCode: "unbind_domain_confirmation_required",
|
|
2137
|
+
operationId: args.operationId,
|
|
2138
|
+
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",
|
|
2139
|
+
data: { preview },
|
|
2140
|
+
nextActions: [
|
|
2141
|
+
{ tool: "unbind_domain", allowed: true, reasonCode: "exact_confirmation_required" }
|
|
2142
|
+
]
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
if (!args.confirmation) throw new Error("confirmation is required for confirm");
|
|
2146
|
+
const result = await ctx.client.unbindDomain(site.siteId, site.credential, {
|
|
2147
|
+
operationId: args.operationId,
|
|
2148
|
+
confirmation: args.confirmation
|
|
2149
|
+
});
|
|
2150
|
+
const { boundDomain: _removed, ...remaining } = site;
|
|
2151
|
+
writeSiteFile(ctx.projectDir, remaining);
|
|
2152
|
+
return structuredToolResult({
|
|
2153
|
+
schemaVersion: 1,
|
|
2154
|
+
outcome: result.servingDeletionPending ? "pending_provider" : "completed",
|
|
2155
|
+
resultCode: "domain_unbound",
|
|
2156
|
+
operationId: args.operationId,
|
|
2157
|
+
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",
|
|
2158
|
+
data: { result },
|
|
2159
|
+
nextActions: [{ tool: "site_status", allowed: true }]
|
|
2160
|
+
});
|
|
2161
|
+
} catch (error) {
|
|
2162
|
+
return toolError(error);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
);
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2005
2168
|
// src/server.ts
|
|
2006
2169
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2007
2170
|
|
|
2008
2171
|
// src/tools/billing.ts
|
|
2009
|
-
import { z as
|
|
2010
|
-
var plan =
|
|
2011
|
-
var trigger = z3.enum(["actual_overage", "credential_automation"]);
|
|
2172
|
+
import { z as z4 } from "zod";
|
|
2173
|
+
var plan = z4.enum(["water", "personal", "share", "business"]);
|
|
2012
2174
|
function registerBillingTools(server, ctx) {
|
|
2013
2175
|
server.registerTool(
|
|
2014
2176
|
"list_billing_plans",
|
|
@@ -2035,93 +2197,37 @@ function registerBillingTools(server, ctx) {
|
|
|
2035
2197
|
}
|
|
2036
2198
|
);
|
|
2037
2199
|
server.registerTool(
|
|
2038
|
-
"
|
|
2200
|
+
"change_subscription_plan",
|
|
2039
2201
|
{
|
|
2040
|
-
description: "
|
|
2202
|
+
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.",
|
|
2041
2203
|
inputSchema: {
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
"create_automation_authorization",
|
|
2045
|
-
"disable_automation",
|
|
2046
|
-
"request_authorized_upgrade"
|
|
2047
|
-
]),
|
|
2048
|
-
operationId: z3.string().min(1).describe("Stable idempotency key chosen by the caller."),
|
|
2049
|
-
targetPlan: plan.optional(),
|
|
2050
|
-
maxPlan: plan.optional(),
|
|
2051
|
-
allowedTriggers: z3.array(trigger).min(1).optional(),
|
|
2052
|
-
observedSnapshotId: z3.string().optional(),
|
|
2053
|
-
observedCatalogVersion: z3.string().optional()
|
|
2204
|
+
targetPlan: plan,
|
|
2205
|
+
operationId: z4.string().min(1)
|
|
2054
2206
|
},
|
|
2055
2207
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
2056
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
2208
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
2057
2209
|
},
|
|
2058
2210
|
async (args) => {
|
|
2059
2211
|
try {
|
|
2060
2212
|
const site = requireSiteFile(ctx);
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
request = {
|
|
2067
|
-
action: args.action,
|
|
2068
|
-
targetPlan: args.targetPlan,
|
|
2069
|
-
operationId: args.operationId
|
|
2070
|
-
};
|
|
2071
|
-
} else if (args.action === "create_automation_authorization") {
|
|
2072
|
-
if (!args.maxPlan || !args.allowedTriggers) {
|
|
2073
|
-
throw new SakupaError(
|
|
2074
|
-
"invalid_request",
|
|
2075
|
-
"maxPlan and allowedTriggers are required for authorization"
|
|
2076
|
-
);
|
|
2077
|
-
}
|
|
2078
|
-
request = {
|
|
2079
|
-
action: args.action,
|
|
2080
|
-
maxPlan: args.maxPlan,
|
|
2081
|
-
allowedTriggers: args.allowedTriggers,
|
|
2082
|
-
operationId: args.operationId
|
|
2083
|
-
};
|
|
2084
|
-
} else if (args.action === "disable_automation") {
|
|
2085
|
-
request = { action: args.action, operationId: args.operationId };
|
|
2086
|
-
} else {
|
|
2087
|
-
if (!args.targetPlan) {
|
|
2088
|
-
throw new SakupaError(
|
|
2089
|
-
"invalid_request",
|
|
2090
|
-
"targetPlan is required for request_authorized_upgrade"
|
|
2091
|
-
);
|
|
2092
|
-
}
|
|
2093
|
-
request = {
|
|
2094
|
-
action: args.action,
|
|
2095
|
-
operationId: args.operationId,
|
|
2096
|
-
targetPlan: args.targetPlan,
|
|
2097
|
-
trigger: "credential_automation",
|
|
2098
|
-
...args.observedSnapshotId ? { observedSnapshotId: args.observedSnapshotId } : {},
|
|
2099
|
-
...args.observedCatalogVersion ? { observedCatalogVersion: args.observedCatalogVersion } : {}
|
|
2100
|
-
};
|
|
2101
|
-
}
|
|
2102
|
-
const result = await ctx.client.manageSubscription(site.siteId, site.credential, request);
|
|
2103
|
-
const portalUrl = "portalUrl" in result ? result.portalUrl : void 0;
|
|
2104
|
-
const authorizationUrl = "authorizationUrl" in result ? result.authorizationUrl : void 0;
|
|
2105
|
-
const executionOutcome = "outcome" in result ? result.outcome : void 0;
|
|
2106
|
-
const outcome = portalUrl || authorizationUrl ? "waiting_user" : executionOutcome === "pending_provider" ? "pending_provider" : executionOutcome === "blocked" ? "blocked" : "completed";
|
|
2107
|
-
const resultCode = "resultCode" in result ? result.resultCode : portalUrl ? "stripe_plan_change_confirmation_required" : authorizationUrl ? "automation_authorization_required" : args.action === "disable_automation" ? "automation_disabled" : "subscription_action_completed";
|
|
2108
|
-
const url = portalUrl ?? authorizationUrl;
|
|
2213
|
+
const result = await ctx.client.changeSubscriptionPlan(site.credential, {
|
|
2214
|
+
siteId: site.siteId,
|
|
2215
|
+
targetPlan: args.targetPlan,
|
|
2216
|
+
operationId: args.operationId
|
|
2217
|
+
});
|
|
2109
2218
|
return structuredToolResult({
|
|
2110
2219
|
schemaVersion: 1,
|
|
2111
|
-
outcome,
|
|
2112
|
-
resultCode,
|
|
2220
|
+
outcome: "waiting_user",
|
|
2221
|
+
resultCode: "stripe_plan_change_confirmation_required",
|
|
2113
2222
|
operationId: args.operationId,
|
|
2114
|
-
summary:
|
|
2115
|
-
data: {
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
expectedOutcome: portalUrl ? "\u7528\u6237\u5728 Stripe \u6258\u7BA1\u9875\u9762\u786E\u8BA4\u540E\uFF0C\u7531 webhook \u66F4\u65B0 Sakupa \u72B6\u6001" : "\u7528\u6237\u5728 Sakupa \u77ED\u65F6\u6548\u9875\u9762\u786E\u8BA4\u7CBE\u786E\u81EA\u52A8\u5347\u7EA7\u6388\u6743"
|
|
2123
|
-
}
|
|
2124
|
-
} : {},
|
|
2223
|
+
summary: `\u5DF2\u751F\u6210\u4ECE ${result.currentPlan} \u5230 ${result.targetPlan} \u7684 Stripe \u786E\u8BA4\u94FE\u63A5\uFF1B\u8BA2\u9605\u5C1A\u672A\u53D8\u66F4\u3002`,
|
|
2224
|
+
data: { result },
|
|
2225
|
+
userAction: {
|
|
2226
|
+
type: "open_url",
|
|
2227
|
+
provider: "stripe",
|
|
2228
|
+
url: result.portalUrl,
|
|
2229
|
+
expectedOutcome: "\u7528\u6237\u5728 Stripe \u6258\u7BA1\u9875\u9762\u786E\u8BA4\u540E\uFF0C\u7531 webhook \u66F4\u65B0 Sakupa \u8BA2\u9605\u72B6\u6001"
|
|
2230
|
+
},
|
|
2125
2231
|
nextActions: [{ tool: "billing_status", allowed: true }]
|
|
2126
2232
|
});
|
|
2127
2233
|
} catch (error) {
|
|
@@ -2145,13 +2251,13 @@ Workflow:
|
|
|
2145
2251
|
3. To make the site PERMANENT, subscribe it to a monthly hosting plan (subscribe_site ->
|
|
2146
2252
|
Stripe-hosted checkout; water/personal/share/business). Paying makes the
|
|
2147
2253
|
{shortId}.sakupa.com URL permanent \u2014 that is what payment buys. Usage over the chosen plan
|
|
2148
|
-
shows an over-limit notice by default.
|
|
2149
|
-
|
|
2254
|
+
shows an over-limit notice by default. An external AI may periodically query usage and
|
|
2255
|
+
recommend a plan, but Sakupa never changes a subscription automatically.
|
|
2150
2256
|
4. Optionally bind a custom domain to the subscribed site (bind_domain): an included extra
|
|
2151
2257
|
serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
|
|
2152
2258
|
first verified request wins; unverified requests expire after 72 hours. billing_status,
|
|
2153
|
-
|
|
2154
|
-
changes are confirmed only on Stripe
|
|
2259
|
+
billing_status, change_subscription_plan, manage_billing and recover_domain_site manage the paid
|
|
2260
|
+
lifecycle. Plan changes are confirmed only on Stripe and synchronized by Stripe webhook.
|
|
2155
2261
|
A cancellation keeps the site paid through the current period. Sakupa reverts it to a free
|
|
2156
2262
|
24h site and removes paid data after Stripe sends the signed final-cancellation webhook.
|
|
2157
2263
|
|
|
@@ -2180,6 +2286,11 @@ function createSakupaMcpServer(opts) {
|
|
|
2180
2286
|
projectDir: opts.projectDir,
|
|
2181
2287
|
apiBaseUrl: opts.apiBaseUrl
|
|
2182
2288
|
});
|
|
2289
|
+
registerLifecycleTools(server, {
|
|
2290
|
+
client,
|
|
2291
|
+
projectDir: opts.projectDir,
|
|
2292
|
+
apiBaseUrl: opts.apiBaseUrl
|
|
2293
|
+
});
|
|
2183
2294
|
return server;
|
|
2184
2295
|
}
|
|
2185
2296
|
export {
|
|
@@ -2191,6 +2302,7 @@ export {
|
|
|
2191
2302
|
createSakupaMcpServer,
|
|
2192
2303
|
deleteSiteFile,
|
|
2193
2304
|
readSiteFile,
|
|
2305
|
+
registerLifecycleTools,
|
|
2194
2306
|
registerTools,
|
|
2195
2307
|
requireSiteFile,
|
|
2196
2308
|
siteFilePath,
|