@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.
Files changed (3) hide show
  1. package/dist/bin.js +217 -100
  2. package/dist/index.js +218 -106
  3. 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.6.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
- await this.call("DELETE", `/v1/sites/${encodeURIComponent(siteId)}`, { credential });
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 manageSubscription(siteId, credential, req) {
641
+ async changeSubscriptionPlan(credential, req) {
633
642
  return this.call(
634
643
  "POST",
635
- `/v1/sites/${encodeURIComponent(siteId)}/billing/manage`,
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 = false;
1206
- 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" : 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";
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: { errorCode, retryable },
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
- "manage_subscription",
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: "Manage this site subscription through explicit actions. open_plan_change returns a Stripe-hosted confirmation URL. create_automation_authorization returns a short-lived Sakupa authorization URL binding the exact cap and triggers. disable_automation is immediate and owner-authorized. request_authorized_upgrade executes only under an existing exact grant, fresh usage snapshot, catalog version and server-side safety gates.",
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: z3.enum([
1992
- "open_plan_change",
1993
- "create_automation_authorization",
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
- let request;
2011
- if (args.action === "open_plan_change") {
2012
- if (!args.targetPlan) {
2013
- throw new SakupaError("invalid_request", "targetPlan is required for open_plan_change");
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
- } else if (args.action === "create_automation_authorization") {
2021
- if (!args.maxPlan || !args.allowedTriggers) {
2022
- throw new SakupaError(
2023
- "invalid_request",
2024
- "maxPlan and allowedTriggers are required for authorization"
2025
- );
2026
- }
2027
- request = {
2028
- action: args.action,
2029
- maxPlan: args.maxPlan,
2030
- allowedTriggers: args.allowedTriggers,
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
- } else if (args.action === "disable_automation") {
2034
- request = { action: args.action, operationId: args.operationId };
2035
- } else {
2036
- if (!args.targetPlan) {
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
- targetPlan: args.targetPlan,
2046
- trigger: "credential_automation",
2047
- ...args.observedSnapshotId ? { observedSnapshotId: args.observedSnapshotId } : {},
2048
- ...args.observedCatalogVersion ? { observedCatalogVersion: args.observedCatalogVersion } : {}
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
- const result = await ctx.client.manageSubscription(site.siteId, site.credential, request);
2052
- const portalUrl = "portalUrl" in result ? result.portalUrl : void 0;
2053
- const authorizationUrl = "authorizationUrl" in result ? result.authorizationUrl : void 0;
2054
- const executionOutcome = "outcome" in result ? result.outcome : void 0;
2055
- const outcome = portalUrl || authorizationUrl ? "waiting_user" : executionOutcome === "pending_provider" ? "pending_provider" : executionOutcome === "blocked" ? "blocked" : "completed";
2056
- 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";
2057
- const url = portalUrl ?? authorizationUrl;
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: url ? "\u5DF2\u521B\u5EFA\u77ED\u65F6\u6548\u6258\u7BA1\u786E\u8BA4\u94FE\u63A5\uFF1B\u5F53\u524D\u8BA2\u9605\u5C1A\u672A\u56E0\u521B\u5EFA\u94FE\u63A5\u800C\u6539\u53D8\u3002" : `\u8BA2\u9605\u52A8\u4F5C\u7ED3\u679C\uFF1A${resultCode}`,
2064
- data: { action: args.action, result },
2065
- ...url ? {
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. Automatic upgrades require a separate, bounded,
2151
- one-time Sakupa authorization and can never exceed the user-approved plan cap.
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
- manage_subscription, manage_billing and recover_domain_site manage the paid lifecycle. Manual plan
2156
- changes are confirmed only on Stripe Customer Portal and synchronized by Stripe webhook.
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.6.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
- await this.call("DELETE", `/v1/sites/${encodeURIComponent(siteId)}`, { credential });
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 manageSubscription(siteId, credential, req) {
689
+ async changeSubscriptionPlan(credential, req) {
681
690
  return this.call(
682
691
  "POST",
683
- `/v1/sites/${encodeURIComponent(siteId)}/billing/manage`,
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 = false;
1250
- 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" : 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";
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: { errorCode, retryable },
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 z3 } from "zod";
2010
- var plan = z3.enum(["water", "personal", "share", "business"]);
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
- "manage_subscription",
2200
+ "change_subscription_plan",
2039
2201
  {
2040
- description: "Manage this site subscription through explicit actions. open_plan_change returns a Stripe-hosted confirmation URL. create_automation_authorization returns a short-lived Sakupa authorization URL binding the exact cap and triggers. disable_automation is immediate and owner-authorized. request_authorized_upgrade executes only under an existing exact grant, fresh usage snapshot, catalog version and server-side safety gates.",
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
- action: z3.enum([
2043
- "open_plan_change",
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: true, openWorldHint: true }
2208
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
2057
2209
  },
2058
2210
  async (args) => {
2059
2211
  try {
2060
2212
  const site = requireSiteFile(ctx);
2061
- let request;
2062
- if (args.action === "open_plan_change") {
2063
- if (!args.targetPlan) {
2064
- throw new SakupaError("invalid_request", "targetPlan is required for open_plan_change");
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: url ? "\u5DF2\u521B\u5EFA\u77ED\u65F6\u6548\u6258\u7BA1\u786E\u8BA4\u94FE\u63A5\uFF1B\u5F53\u524D\u8BA2\u9605\u5C1A\u672A\u56E0\u521B\u5EFA\u94FE\u63A5\u800C\u6539\u53D8\u3002" : `\u8BA2\u9605\u52A8\u4F5C\u7ED3\u679C\uFF1A${resultCode}`,
2115
- data: { action: args.action, result },
2116
- ...url ? {
2117
- userAction: {
2118
- type: "open_url",
2119
- provider: portalUrl ? "stripe" : "sakupa",
2120
- url,
2121
- ..."expiresAt" in result && typeof result.expiresAt === "string" ? { expiresAt: result.expiresAt } : {},
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. Automatic upgrades require a separate, bounded,
2149
- one-time Sakupa authorization and can never exceed the user-approved plan cap.
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
- manage_subscription, manage_billing and recover_domain_site manage the paid lifecycle. Manual plan
2154
- changes are confirmed only on Stripe Customer Portal and synchronized by Stripe webhook.
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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakupa/mcp",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Sakupa MCP server: publish AI-made static sites from your AI tool. AI-made pages, live in seconds.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",