@sakupa/mcp 0.7.48 → 0.7.49

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 +132 -46
  2. package/dist/index.js +134 -46
  3. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -120,7 +120,7 @@ function resolveLockedProjectRoot(projectDir) {
120
120
  if (markerState.kind === "absent") {
121
121
  throw new ProjectRootError(
122
122
  "not_initialized",
123
- `The MCP working directory ${canonical} is not initialized. Run \`npx -y @sakupa/mcp@latest init\` in that directory; do not pass a path argument.`
123
+ `The MCP working directory ${canonical} is not initialized. Call the init MCP tool with no path argument. If help confirms that this client has no MCP Roots, the AI may run \`npx -y @sakupa/mcp@latest init\` itself as the fallback.`
124
124
  );
125
125
  }
126
126
  return {
@@ -400,7 +400,7 @@ function isFreeSiteAllowanceNetworkReference(value) {
400
400
  }
401
401
 
402
402
  // ../core/dist/domain/version.js
403
- var SAKUPA_MCP_VERSION = "0.7.48";
403
+ var SAKUPA_MCP_VERSION = "0.7.49";
404
404
 
405
405
  // ../core/dist/domain/errors.js
406
406
  var HTTP_STATUS = {
@@ -729,6 +729,7 @@ var EPHEMERAL_RETENTION_HOURS = 90 * 24;
729
729
 
730
730
  // src/config.ts
731
731
  var TEST_API_BASE_URL = "https://api-test.sakupa.com";
732
+ var PRODUCTION_API_BASE_URL = DEFAULT_API_BASE_URL;
732
733
  function previewHostPatternFor(apiBaseUrl) {
733
734
  return environmentFor(apiBaseUrl) === "test" ? "{shortId}-test.sakupa.com" : "{shortId}.sakupa.com";
734
735
  }
@@ -743,6 +744,11 @@ function loadMcpRuntimeConfig(env = process.env) {
743
744
  }
744
745
  return { apiBaseUrl, testAccessToken };
745
746
  }
747
+ if (apiBaseUrl !== PRODUCTION_API_BASE_URL) {
748
+ throw new Error(
749
+ `Unsupported Sakupa API origin: ${apiBaseUrl}. Only ${PRODUCTION_API_BASE_URL} and ${TEST_API_BASE_URL} are accepted; refusing to classify an unknown endpoint as Production.`
750
+ );
751
+ }
746
752
  if (testAccessToken.length > 0) {
747
753
  throw new Error(
748
754
  `SAKUPA_TEST_ACCESS_TOKEN may only be used with ${TEST_API_BASE_URL}. Remove it before connecting to any other API.`
@@ -751,7 +757,9 @@ function loadMcpRuntimeConfig(env = process.env) {
751
757
  return { apiBaseUrl };
752
758
  }
753
759
  function environmentFor(apiBaseUrl) {
754
- return apiBaseUrl === TEST_API_BASE_URL ? "test" : "production";
760
+ if (apiBaseUrl === TEST_API_BASE_URL) return "test";
761
+ if (apiBaseUrl === PRODUCTION_API_BASE_URL) return "production";
762
+ return "unknown";
755
763
  }
756
764
 
757
765
  // src/server.ts
@@ -3122,7 +3130,7 @@ var ProjectBindingResolver = class {
3122
3130
  snapshot,
3123
3131
  this.processCwd,
3124
3132
  rootCandidates,
3125
- "This IDE did not provide MCP Roots and the MCP process cwd is not an initialized project. Do not write into the IDE installation directory. Run `npx -y @sakupa/mcp@latest init` with no path arguments from the intended project terminal, then configure/restart the MCP process in that directory."
3133
+ "This IDE did not provide MCP Roots and the MCP process cwd is not an initialized project. Do not write into the IDE installation directory. Run help from the intended project context. If help confirms the missing-Roots diagnosis, the AI may run `npx -y @sakupa/mcp@latest init` with no path arguments from that directory. Never ask the user to run it."
3126
3134
  )
3127
3135
  };
3128
3136
  }
@@ -3231,8 +3239,16 @@ var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
3231
3239
  data: z.record(z.string(), z.unknown()),
3232
3240
  presentation: z.object({
3233
3241
  userLanguage: z.literal("infer_from_conversation"),
3242
+ userTimeZone: z.literal("infer_from_conversation"),
3243
+ answerScope: z.literal("current_user_question"),
3244
+ retainTechnicalContext: z.literal(true),
3245
+ productionEnvironmentDisclosure: z.literal("only_when_asked_or_relevant"),
3246
+ nonProductionEnvironmentDisclosure: z.literal("always"),
3234
3247
  translateFields: z.array(z.string()),
3235
- preserveExactFields: z.array(z.string())
3248
+ preserveExactFields: z.array(z.string()),
3249
+ mustTellUser: z.array(z.string()),
3250
+ tellWhenRelevant: z.array(z.string()),
3251
+ agentInstructions: z.array(z.string())
3236
3252
  }).optional(),
3237
3253
  decision: z.object({
3238
3254
  decisionVersion: z.literal(1),
@@ -3287,9 +3303,54 @@ var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
3287
3303
  )
3288
3304
  };
3289
3305
  function structuredToolResult(envelope) {
3306
+ const presentation = {
3307
+ userLanguage: "infer_from_conversation",
3308
+ userTimeZone: "infer_from_conversation",
3309
+ answerScope: "current_user_question",
3310
+ retainTechnicalContext: true,
3311
+ productionEnvironmentDisclosure: "only_when_asked_or_relevant",
3312
+ nonProductionEnvironmentDisclosure: "always",
3313
+ translateFields: [
3314
+ "summary",
3315
+ "userAction.expectedOutcome",
3316
+ "userAction.options[].label",
3317
+ "userAction.options[].expectedOutcome",
3318
+ ...envelope.presentation?.translateFields ?? []
3319
+ ],
3320
+ preserveExactFields: [
3321
+ "data",
3322
+ "nextActions",
3323
+ "operationId",
3324
+ "embedded URLs, paths, IDs, code tokens, tool names, arguments, DNS values, field names, amounts and exact timestamps",
3325
+ ...envelope.presentation?.preserveExactFields ?? []
3326
+ ],
3327
+ mustTellUser: [
3328
+ "the requested result and whether the operation changed anything",
3329
+ "any action the user must personally complete",
3330
+ "material charges, deadlines, cancellation, replacement, credential revocation and other security or irreversible consequences",
3331
+ ...envelope.presentation?.mustTellUser ?? []
3332
+ ],
3333
+ tellWhenRelevant: [
3334
+ "local paths, internal IDs, raw enums, exact arguments, provider state and diagnostic details",
3335
+ "normal Production environment details, unless the user asks or environment identity matters to the task",
3336
+ ...envelope.presentation?.tellWhenRelevant ?? []
3337
+ ],
3338
+ agentInstructions: [
3339
+ "Keep every technical fact available for reasoning and exact continuation; never discard it merely because it is technical.",
3340
+ "Answer the current user question instead of mechanically dumping every returned field. Explain relevant technical terms in plain language while retaining the exact term when it helps accuracy.",
3341
+ "Do not ask the user to copy or run tool arguments that this AI can execute. Use exact nextActions internally after any required user choice.",
3342
+ "Translate human-readable explanations into the language the user is using and present dates and sizes in locally understandable form alongside exact values when precision matters.",
3343
+ "Do not proactively mention a normal Production environment. Always disclose Test or unknown/non-production environments prominently.",
3344
+ ...envelope.presentation?.agentInstructions ?? []
3345
+ ]
3346
+ };
3347
+ const structuredEnvelope = { ...envelope, presentation };
3348
+ const presentationFallback = "AGENT PRESENTATION CONTRACT: Retain every technical fact below for reasoning and exact follow-up. Tell the user the requested result, required next step, and every material charge, deadline, security or irreversible consequence. Answer only the current question instead of dumping all fields; explain relevant technical terms in plain language while keeping exact terms available. Translate human-readable explanations into the user's language, but preserve URLs, paths, IDs, tool names, arguments, DNS values, field names, amounts and exact timestamps. Do not proactively show a normal Production environment; always disclose Test or unknown environments. Never ask the user to run exact tool arguments that this AI can execute.";
3290
3349
  return {
3291
- content: [{ type: "text", text: envelope.summary }],
3292
- structuredContent: envelope
3350
+ content: [{ type: "text", text: `${envelope.summary}
3351
+
3352
+ ${presentationFallback}` }],
3353
+ structuredContent: structuredEnvelope
3293
3354
  };
3294
3355
  }
3295
3356
 
@@ -3440,14 +3501,22 @@ function toolError(e) {
3440
3501
  const timeoutSummary = timedOut ? timeoutRetrySafe ? `${timeoutOperation} exceeded its ${timeoutMs ?? "configured"}ms deadline. Sakupa made zero automatic retries; this was a read, so retry it manually or run help if it repeats.` : `${timeoutOperation} exceeded its ${timeoutMs ?? "configured"}ms deadline. Sakupa made zero automatic retries. The remote outcome may be unknown; do not repeat the write automatically. Query current status or run help first.` : void 0;
3441
3502
  const serverGuidance = isSakupaError(e) && errorCode !== "internal" && errorCode !== "unauthorized" && errorCode !== "upgrade_required" && e.message.trim().length > 0 ? e.message : void 0;
3442
3503
  const safeSummary = timeoutSummary ?? (e instanceof LocalGuidanceError ? e.message : errorCode === "upgrade_required" ? `This Sakupa MCP client is v${MCP_VERSION}, older than the server's minimum supported version${minimumVersion !== void 0 ? ` (v${minimumVersion})` : ""}, so the server refused the call. To fix it: ask the user to fully restart their MCP client session \u2014 "npx -y @sakupa/mcp@latest" setups fetch the current version on restart (run "npx clear-npx-cache" first if the old version persists); global installs need "npm install -g @sakupa/mcp@latest". After the restart, retry this exact tool call.` : errorCode === "unauthorized" ? UNAUTHORIZED_SUMMARY : serverGuidance ?? (retryable ? "An upstream service is temporarily unavailable or busy; retry shortly." : opaqueUnclassified ? "This failed with an error Sakupa could not classify, and retrying the same call will not help. Run help with the failed tool and error code first; only use report if help explicitly recommends it." : "The operation failed; no server-internal details are exposed."));
3504
+ const customerMeaning = timedOut ? timeoutRetrySafe ? "Sakupa did not receive this read result before the deadline; no automatic retry occurred." : "Sakupa did not receive a final result before the deadline, so the AI must check current state before attempting another write." : errorCode === "unauthorized" ? "The cloud site is still intact, but this project no longer has a valid management credential for it." : errorCode === "upgrade_required" ? "The installed Sakupa MCP version is too old for the current API and must be refreshed before retrying." : errorCode === "payment_required" ? "This action needs an active subscription or a payment issue must be resolved first." : errorCode === "rate_limited" ? "Sakupa temporarily refused this operation because a usage or frequency limit was reached." : errorCode === "not_found" ? "The requested Sakupa site, project binding, or operation could not be found." : errorCode === "forbidden" ? "Sakupa refused this operation because the current authority or site state does not allow it." : errorCode === "conflict" || errorCode === "state_conflict" || errorCode === "confirmation_required" ? "Sakupa safely stopped because the site, billing state, or required confirmation no longer matches." : errorCode === "invalid_request" || errorCode === "validation_failed" ? "Sakupa could not complete the operation because required input or current state was invalid." : retryable ? "A temporary Sakupa dependency problem prevented completion." : "Sakupa did not complete the operation; use the retained diagnostics to determine the safe next step.";
3505
+ const userFacingSummary = `Customer meaning: ${customerMeaning}
3506
+ Technical context for the AI: ${safeSummary}`;
3443
3507
  const result = structuredToolResult({
3444
3508
  schemaVersion: 1,
3445
3509
  outcome: "failed",
3446
3510
  resultCode: `error_${errorCode}`,
3447
- summary: safeSummary,
3511
+ summary: userFacingSummary,
3448
3512
  data: {
3449
3513
  errorCode,
3450
3514
  retryable,
3515
+ customerMeaning,
3516
+ diagnosticContext: {
3517
+ technicalMessage: safeSummary,
3518
+ source: serverGuidance ? "sakupa_business_guidance" : "mcp_safe_diagnostic"
3519
+ },
3451
3520
  ...safeDetails && Object.keys(safeDetails).length > 0 ? { details: safeDetails } : {}
3452
3521
  },
3453
3522
  nextActions: [
@@ -3464,16 +3533,11 @@ function toolError(e) {
3464
3533
 
3465
3534
  // src/tools/decision.ts
3466
3535
  var DECISION_PRESENTATION_POLICY = {
3467
- userLanguage: "infer_from_conversation",
3468
3536
  translateFields: [
3469
- "summary",
3470
3537
  "decision.prompt",
3471
3538
  "decision.options[].label",
3472
3539
  "decision.options[].description",
3473
- "decision.options[].consequences[]",
3474
- "userAction.expectedOutcome",
3475
- "userAction.options[].label",
3476
- "userAction.options[].expectedOutcome"
3540
+ "decision.options[].consequences[]"
3477
3541
  ],
3478
3542
  preserveExactFields: [
3479
3543
  "decision.options[].id",
@@ -3481,7 +3545,11 @@ var DECISION_PRESENTATION_POLICY = {
3481
3545
  "userAction.resumeWith",
3482
3546
  "userAction.options[].value",
3483
3547
  "nextActions",
3484
- "embedded URLs, code tokens, field names and confirmation values"
3548
+ "decision option IDs and confirmation values"
3549
+ ],
3550
+ mustTellUser: ["every decision option, with no option selected by default"],
3551
+ agentInstructions: [
3552
+ "Wait for the user's selection, then copy only that option's exact nextAction without reconstructing arguments."
3485
3553
  ]
3486
3554
  };
3487
3555
  var FORBIDDEN_DECISION_ARGUMENT_KEYS = /* @__PURE__ */ new Set([
@@ -3573,9 +3641,7 @@ function formatDecisionFallback(decision) {
3573
3641
  ${option.description}${consequences}
3574
3642
  ${exactAction}`;
3575
3643
  });
3576
- return `LANGUAGE PRESENTATION RULE: Translate all human-readable summaries, prompts, labels, descriptions and consequences into the language the user is using before presenting them. Keep option IDs, tool names, arguments, URLs, field names and confirmation values exactly unchanged.
3577
-
3578
- USER DECISION REQUIRED: ${decision.prompt}
3644
+ return `USER DECISION REQUIRED: ${decision.prompt}
3579
3645
  No option is selected by default. Present every option to the user, do not choose on their behalf, and never reconstruct or guess tool arguments.
3580
3646
 
3581
3647
  ` + options.join("\n\n");
@@ -3684,6 +3750,15 @@ var severityEnum = z2.enum(["low", "medium", "high", "critical"]);
3684
3750
  function planCatalog() {
3685
3751
  return TIER_ORDER.map((p) => `${p} JPY ${tierPriceJpy(p)}/month`).join(", ");
3686
3752
  }
3753
+ function deploymentEnvironmentContext(apiBaseUrl) {
3754
+ const environment = environmentFor(apiBaseUrl);
3755
+ if (environment === "production") {
3756
+ return `Agent context \u2014 environment: PRODUCTION (${apiBaseUrl}). Retain this fact, but do not proactively show it to the user unless asked or relevant.
3757
+ `;
3758
+ }
3759
+ return `IMPORTANT USER NOTICE \u2014 environment: ${environment.toUpperCase()} (${apiBaseUrl}). The user must be told prominently that this is not the normal Production environment.
3760
+ `;
3761
+ }
3687
3762
  var ticketCategoryEnum = z2.enum([
3688
3763
  "billing",
3689
3764
  "payment",
@@ -3934,7 +4009,7 @@ Next action: ${analysis.suggestedNextAction}`,
3934
4009
  server.registerTool(
3935
4010
  "deploy",
3936
4011
  {
3937
- 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://${previewHostPattern}) 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 first and refuses to upload source projects, secrets, .env files, archives, media or server code. The MCP process is locked to the current directory initialized by \`npx -y @sakupa/mcp@latest init\`; no tool argument can change that root. outputDir is a separate REQUIRED relative path supplied from the current project inspection. Never uploads anything when analysis says the project is not deployable.`,
4012
+ 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://${previewHostPattern}) and stores the management credential in .sakupa/site.json. Later runs update the existing site (free sites also refresh their validity; subscription-backed sites have no free-site expiry while the subscription remains active). Runs analyze first and refuses to upload source projects, secrets, .env files, archives, media or server code. The MCP process is locked to the current directory initialized by the no-argument init MCP tool; no tool argument can change that root. outputDir is a separate REQUIRED relative path supplied from the current project inspection. Never uploads anything when analysis says the project is not deployable.`,
3938
4013
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
3939
4014
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
3940
4015
  inputSchema: {
@@ -4425,12 +4500,11 @@ Next action: ${analysis.suggestedNextAction}`,
4425
4500
  return text(
4426
4501
  "site_published",
4427
4502
  `Site published: ${finalized2.url}
4428
- Environment: ${environmentFor(ctx.apiBaseUrl).toUpperCase()} (${ctx.apiBaseUrl})
4429
- Project directory: ${ctx.projectDir}
4503
+ ` + deploymentEnvironmentContext(ctx.apiBaseUrl) + `Project directory: ${ctx.projectDir}
4430
4504
  Files uploaded: ${uploaded2} (${finalized2.totalBytes} bytes)
4431
4505
  ` + (finalized2.expiresAt ? `Expires at: ${finalized2.expiresAt}
4432
4506
  ` : "") + `
4433
- This is a FREE temporary preview: it stays live for ${FREE_SITE_TTL_HOURS} hours. Deploying again or calling refresh extends the validity; subscribing the site (subscribe) 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.
4507
+ This is a FREE temporary preview: it stays live for ${FREE_SITE_TTL_HOURS} hours. Deploying again or calling refresh extends the validity; while a subscription remains active, this URL stays live without the free-site expiry. The management credential was saved to .sakupa/site.json \u2014 keep that file: it is the only way to manage this site.
4434
4508
  ` + credentialGitReminder(ctx.projectDir) + (finalized2.warnings.length > 0 ? `
4435
4509
  Warnings:
4436
4510
  ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
@@ -4515,14 +4589,13 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
4515
4589
  return text(
4516
4590
  handoffPerformed ? "free_site_slot_reassigned" : "site_updated",
4517
4591
  `Site updated: ${finalized.url}
4518
- Environment: ${environmentFor(ctx.apiBaseUrl).toUpperCase()} (${ctx.apiBaseUrl})
4519
- Project directory: ${ctx.projectDir}
4592
+ ` + deploymentEnvironmentContext(ctx.apiBaseUrl) + `Project directory: ${ctx.projectDir}
4520
4593
  Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
4521
4594
  ` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
4522
4595
  ` : "") + (credentialRelocatedFrom.length > 0 ? `Credential binding relocated from ${credentialRelocatedFrom.join(", ")} to ${ctx.projectDir}/.sakupa; the existing site was preserved.
4523
4596
  ` : "") + (handoffPerformed ? `Site handoff completed from the authenticated device list. The existing free-site URL stayed the same, the cloud site was NOT deleted, and its content was replaced. Sakupa issued a fresh project credential and revoked ${handoffRevokedCredentials} previous credential(s), so no old project can continue managing this URL.` + (handoffCleanup?.sourceCredentialRemoved ? " A matching obsolete local site.json was removed automatically.\n" : "\n") : "") + (credentialRotationResumed ? "A previously confirmed credential rotation was resumed safely before this deploy; every older credential is revoked.\n" : "") + (finalized.mode === "free" ? `
4524
- Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh call. Subscribing (subscribe) makes the site permanent.
4525
- ` : "\nThis site is subscribed and permanent \u2014 no expiry.\n") + (finalized.warnings.length > 0 ? `
4597
+ Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh call. While a subscription remains active, the site stays live without this free-site expiry.
4598
+ ` : "\nThis site is subscription-backed and has no free-site expiry while the subscription remains active.\n") + (finalized.warnings.length > 0 ? `
4526
4599
  Warnings:
4527
4600
  ${JSON.stringify(finalized.warnings, null, 2)}` : "") + (credentialSecurity?.rotationRecommended ? `
4528
4601
 
@@ -4578,7 +4651,7 @@ Optional security recommendation: this management credential was created at ${cr
4578
4651
  server.registerTool(
4579
4652
  "refresh",
4580
4653
  {
4581
- 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.",
4654
+ description: "Refresh the validity of the free temporary site WITHOUT uploading content. Uses the local credential in .sakupa/site.json. Subscription-backed sites have no free-site expiry while the subscription remains active and need no refresh.",
4582
4655
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4583
4656
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
4584
4657
  inputSchema: {}
@@ -4642,7 +4715,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
4642
4715
  server.registerTool(
4643
4716
  "subscribe",
4644
4717
  {
4645
- description: `Create a Stripe Checkout link that subscribes THIS site to a Sakupa Hosting monthly plan (${planCatalog()}). Paying makes the site PERMANENT on its ${previewHostPattern} URL \u2014 no more 24h expiry; that is the core value of paying. Binding a custom domain afterwards (bind) 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 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.`,
4718
+ description: `Create a Stripe Checkout link that subscribes THIS site to a Sakupa Hosting monthly plan (${planCatalog()}). While the subscription remains active, its ${previewHostPattern} URL stays live without the free 24-hour expiry. Binding a custom domain afterwards (bind) 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 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.`,
4646
4719
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4647
4720
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
4648
4721
  inputSchema: {
@@ -4669,7 +4742,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
4669
4742
  ${res.checkoutUrl}
4670
4743
 
4671
4744
  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.
4672
- Once payment confirms, the site becomes permanent on its current URL. Binding a custom domain (bind) is optional and still requires DNS verification.`,
4745
+ Once Stripe confirms payment and Sakupa synchronizes the subscription, the current URL stays live while that subscription remains active. Binding a custom domain (bind) is optional and still requires DNS verification.`,
4673
4746
  {
4674
4747
  siteId: res.siteId,
4675
4748
  plan: res.plan,
@@ -4688,7 +4761,7 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
4688
4761
  server.registerTool(
4689
4762
  "bind",
4690
4763
  {
4691
- description: `Bind a custom domain to this subscribed site \u2014 an OPTIONAL extra serving surface; the permanent ${previewHostPattern} URL keeps working alongside it. The binding unit is the APEX domain: binding example.com reserves routes for example.com and www.example.com, but ONLY www is required and judged for activation; the naked apex is optional because many DNS providers cannot point it. 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). Ownership is proven ONLY by DNS control of the apex \u2014 payment never grants ownership, and bindings are ALWAYS challengeable: whoever proves CURRENT DNS control takes the domain, even from an existing binding (the displaced site keeps its subscription, content and permanent URL). Unverified requests expire after 72 hours. Call again with action "status" to check progress.`,
4764
+ description: `Bind a custom domain to this subscribed site \u2014 an OPTIONAL extra serving surface; the subscription-backed ${previewHostPattern} URL keeps working alongside it while the subscription is active. The binding unit is the APEX domain: binding example.com reserves routes for example.com and www.example.com, but ONLY www is required and judged for activation; the naked apex is optional because many DNS providers cannot point it. 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). Ownership is proven ONLY by DNS control of the apex \u2014 payment never grants ownership, and bindings are ALWAYS challengeable: whoever proves CURRENT DNS control takes the domain, even from an existing binding (the displaced site keeps its subscription, content and subscription-backed Sakupa URL). Unverified requests expire after 72 hours. Call again with action "status" to check progress.`,
4692
4765
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4693
4766
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
4694
4767
  inputSchema: {
@@ -4816,7 +4889,7 @@ When the user says the TXT is set, run bind "status". It verifies ownership and
4816
4889
  noteSiteMode(res.siteId, res.mode);
4817
4890
  const lines = [
4818
4891
  `AUTHORITATIVE BILLING SNAPSHOT for site ${res.siteId} (mode: ${res.mode})`,
4819
- res.permanentUrl ? `Permanent URL: ${res.permanentUrl}` : void 0,
4892
+ res.permanentUrl ? `Subscription-backed Sakupa URL: ${res.permanentUrl}` : void 0,
4820
4893
  res.plan ? `Current plan: ${res.plan} (JPY ${res.monthlyPriceJpy ?? tierPriceJpy(res.plan)}/month)` : "Current plan: (no subscription yet)",
4821
4894
  res.subscriptionStatus ? `Subscription payment state: ${res.subscriptionStatus}` : void 0,
4822
4895
  res.cancelAtPeriodEnd === true ? `Renewal: CANCELED \u2014 the site reverts to free at ${res.cancellationEffectiveAt ?? res.currentPeriodEnd ?? "the end of the already-paid month"}` : res.renewalPlan ? `Next renewal plan: ${res.renewalPlan} (JPY ${res.renewalMonthlyPriceJpy ?? tierPriceJpy(res.renewalPlan)}/month)` + (res.renewalEffectiveAt ? `, effective ${res.renewalEffectiveAt}` : " (unchanged)") : res.cancelAtPeriodEnd === false ? "Renewal cancellation: no" : void 0,
@@ -5515,7 +5588,7 @@ var TOOL_MANUALS = {
5515
5588
  init: {
5516
5589
  purpose: "Initialize the active IDE workspace as one Sakupa project.",
5517
5590
  sideEffects: "Creates only .sakupa/project.json locally; no API call, site or charge.",
5518
- preconditions: "Exactly one usable MCP workspace Root. Clients without Roots use CLI init.",
5591
+ preconditions: "Exactly one usable MCP workspace Root. If Roots are unavailable, help may authorize the AI to use CLI init.",
5519
5592
  parameterNames: [],
5520
5593
  parameters: "No parameters and no path argument.",
5521
5594
  warnings: [
@@ -5564,7 +5637,9 @@ var TOOL_MANUALS = {
5564
5637
  preconditions: "A valid local site credential.",
5565
5638
  parameterNames: [],
5566
5639
  parameters: "No parameters.",
5567
- warnings: ["Subscribed sites are permanent and do not need refresh."],
5640
+ warnings: [
5641
+ "Subscription-backed sites have no free-site expiry while the subscription remains active and do not need refresh."
5642
+ ],
5568
5643
  nextStep: "Call status to verify the new expiry."
5569
5644
  },
5570
5645
  status: {
@@ -5706,7 +5781,7 @@ function registerHelpTools(server, baseCtx) {
5706
5781
  server.registerTool(
5707
5782
  "init",
5708
5783
  {
5709
- description: "Initialize the active MCP workspace Root as a Sakupa project. Takes no path argument, creates only .sakupa/project.json at that exact Root, preserves site/recovery state, makes no API call and is idempotent. Clients without MCP Roots must use the no-argument CLI init.",
5784
+ description: "Initialize the active MCP workspace Root as a Sakupa project. Takes no path argument, creates only .sakupa/project.json at that exact Root, preserves site/recovery state, makes no API call and is idempotent. If MCP Roots are unavailable, call help; the AI may then use the no-argument CLI init itself.",
5710
5785
  inputSchema: {},
5711
5786
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5712
5787
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
@@ -6183,14 +6258,14 @@ Workflow:
6183
6258
  current deployment and serving state at any time. Every update checks the credential's
6184
6259
  server-side issue time. When it is older than 7 days, deploy succeeds and only SUGGESTS the
6185
6260
  optional rotate tool; never rotate without the user's explicit confirmation.
6186
- 3. To make the site PERMANENT, subscribe it to a monthly hosting plan (plans
6261
+ 3. To keep the site live beyond the free period, subscribe it to a monthly hosting plan (plans
6187
6262
  shows the catalog; subscribe -> Stripe-hosted checkout;
6188
- water/personal/share/business). Paying makes the
6189
- ${hostPattern} URL permanent \u2014 that is what payment buys. Usage over the chosen plan
6263
+ water/personal/share/business). While the subscription remains active, the
6264
+ ${hostPattern} URL stays live without the free 24-hour expiry. Usage over the chosen plan
6190
6265
  shows an over-limit notice by default. An external AI may periodically query usage and
6191
6266
  recommend a plan, but Sakupa never changes a subscription automatically.
6192
6267
  4. Optionally bind a custom domain to the subscribed site (bind): an included extra
6193
- serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
6268
+ serving surface alongside the subscription-backed Sakupa URL. Ownership is proven only by DNS control; the
6194
6269
  first verified request wins; unverified requests expire after 72 hours. billing,
6195
6270
  change, portal and recover manage the paid lifecycle. Binding a
6196
6271
  NEW domain while one is live is a zero-downtime SWITCH: the old domain keeps serving
@@ -6215,11 +6290,12 @@ translate every human-readable summary, prompt, option label, description and co
6215
6290
  language. Keep option IDs, tool names, exact arguments, URLs, field names and confirmation values
6216
6291
  unchanged.
6217
6292
 
6218
- Project directory contract: before the first deploy or a new recovery, initialize the intended
6219
- project by calling init with NO path argument. init uses the IDE's exact MCP Root and creates the
6220
- non-secret .sakupa/project.json directly there. The CLI command
6221
- "npx -y @sakupa/mcp@latest init" remains the safe fallback for clients without MCP Roots and
6222
- also accepts NO path argument. ONE MCP process = ONE Roots-first locked project = ONE site.
6293
+ Project directory contract: before the first deploy or a new recovery, CALL the init MCP tool with
6294
+ NO path argument. init uses the IDE's exact MCP Root and creates the non-secret
6295
+ .sakupa/project.json directly there. Do not merely print installation or CLI instructions when the
6296
+ init tool is available. Only after help confirms that the client does not provide MCP Roots may the
6297
+ AI itself use the CLI command "npx -y @sakupa/mcp@latest init" as a fallback; never ask the user to
6298
+ run it. The CLI also accepts NO path argument. ONE MCP process = ONE Roots-first locked project = ONE site.
6223
6299
  Site tools do not accept projectDir and cannot select another root; help, plans and report preview
6224
6300
  and public_recovery portal remain project-independent.
6225
6301
  Sakupa stores .sakupa/site.json and recovery state only in the locked directory; it never uses
@@ -6232,11 +6308,21 @@ before retrying with outputDirChangeConfirmed: true.
6232
6308
  After init, require sakupaAtProjectRoot=true. Before deploy, Sakupa checks every directory segment
6233
6309
  between the project Root and outputDir for a misplaced .sakupa and safely relocates only validated,
6234
6310
  non-conflicting state; never copy, delete or overwrite site.json by shell command.
6235
- After every deploy, TELL the user which environment it went to (deploy results carry an
6236
- Explicit Environment line: TEST vs PRODUCTION). analyze, deploy, status,
6237
- and refresh echo
6311
+ Every result retains the exact environment for AI reasoning. Prominently tell the user when the
6312
+ environment is TEST or unknown/non-production. Do not proactively mention a normal PRODUCTION
6313
+ environment unless the user asks or the distinction is relevant to the current question. analyze,
6314
+ deploy, status, and refresh echo
6238
6315
  the Roots-first locked directory they acted on.
6239
6316
 
6317
+ Presentation contract: preserve every returned technical fact for reasoning and exact continuation,
6318
+ including paths, IDs, enums, tool names, arguments, DNS values, provider states, amounts and exact
6319
+ timestamps. Do not mechanically dump every field. Answer the user's current question first, explain
6320
+ relevant technical terms in plain language while retaining the exact term, translate human-readable
6321
+ explanations into the user's language, and always disclose material charges, deadlines, cancellation,
6322
+ replacement, credential revocation, security effects and irreversible consequences. A technical fact
6323
+ may be omitted from the immediate user-facing answer only when it is irrelevant to the current
6324
+ question; it must remain available in the tool result for follow-up.
6325
+
6240
6326
  On any difficulty, call help before retrying or escalating. Only offer report when help returns
6241
6327
  reportRecommended:true; attach your own factual account via agentContext and show the exact
6242
6328
  sanitized preview before asking the user to confirm submission.
package/dist/index.js CHANGED
@@ -145,7 +145,7 @@ function isFreeSiteAllowanceNetworkReference(value) {
145
145
  }
146
146
 
147
147
  // ../core/dist/domain/version.js
148
- var SAKUPA_MCP_VERSION = "0.7.48";
148
+ var SAKUPA_MCP_VERSION = "0.7.49";
149
149
 
150
150
  // ../core/dist/domain/errors.js
151
151
  var HTTP_STATUS = {
@@ -478,6 +478,7 @@ var CLIENT_TYPE = "sakupa-mcp";
478
478
 
479
479
  // src/config.ts
480
480
  var TEST_API_BASE_URL = "https://api-test.sakupa.com";
481
+ var PRODUCTION_API_BASE_URL = DEFAULT_API_BASE_URL;
481
482
  function previewHostPatternFor(apiBaseUrl) {
482
483
  return environmentFor(apiBaseUrl) === "test" ? "{shortId}-test.sakupa.com" : "{shortId}.sakupa.com";
483
484
  }
@@ -492,6 +493,11 @@ function loadMcpRuntimeConfig(env = process.env) {
492
493
  }
493
494
  return { apiBaseUrl, testAccessToken };
494
495
  }
496
+ if (apiBaseUrl !== PRODUCTION_API_BASE_URL) {
497
+ throw new Error(
498
+ `Unsupported Sakupa API origin: ${apiBaseUrl}. Only ${PRODUCTION_API_BASE_URL} and ${TEST_API_BASE_URL} are accepted; refusing to classify an unknown endpoint as Production.`
499
+ );
500
+ }
495
501
  if (testAccessToken.length > 0) {
496
502
  throw new Error(
497
503
  `SAKUPA_TEST_ACCESS_TOKEN may only be used with ${TEST_API_BASE_URL}. Remove it before connecting to any other API.`
@@ -500,7 +506,9 @@ function loadMcpRuntimeConfig(env = process.env) {
500
506
  return { apiBaseUrl };
501
507
  }
502
508
  function environmentFor(apiBaseUrl) {
503
- return apiBaseUrl === TEST_API_BASE_URL ? "test" : "production";
509
+ if (apiBaseUrl === TEST_API_BASE_URL) return "test";
510
+ if (apiBaseUrl === PRODUCTION_API_BASE_URL) return "production";
511
+ return "unknown";
504
512
  }
505
513
 
506
514
  // src/timeout.ts
@@ -1646,7 +1654,7 @@ function resolveLockedProjectRoot(projectDir) {
1646
1654
  if (markerState.kind === "absent") {
1647
1655
  throw new ProjectRootError(
1648
1656
  "not_initialized",
1649
- `The MCP working directory ${canonical} is not initialized. Run \`npx -y @sakupa/mcp@latest init\` in that directory; do not pass a path argument.`
1657
+ `The MCP working directory ${canonical} is not initialized. Call the init MCP tool with no path argument. If help confirms that this client has no MCP Roots, the AI may run \`npx -y @sakupa/mcp@latest init\` itself as the fallback.`
1650
1658
  );
1651
1659
  }
1652
1660
  return {
@@ -1932,7 +1940,7 @@ var ProjectBindingResolver = class {
1932
1940
  snapshot,
1933
1941
  this.processCwd,
1934
1942
  rootCandidates,
1935
- "This IDE did not provide MCP Roots and the MCP process cwd is not an initialized project. Do not write into the IDE installation directory. Run `npx -y @sakupa/mcp@latest init` with no path arguments from the intended project terminal, then configure/restart the MCP process in that directory."
1943
+ "This IDE did not provide MCP Roots and the MCP process cwd is not an initialized project. Do not write into the IDE installation directory. Run help from the intended project context. If help confirms the missing-Roots diagnosis, the AI may run `npx -y @sakupa/mcp@latest init` with no path arguments from that directory. Never ask the user to run it."
1936
1944
  )
1937
1945
  };
1938
1946
  }
@@ -2041,8 +2049,16 @@ var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
2041
2049
  data: z.record(z.string(), z.unknown()),
2042
2050
  presentation: z.object({
2043
2051
  userLanguage: z.literal("infer_from_conversation"),
2052
+ userTimeZone: z.literal("infer_from_conversation"),
2053
+ answerScope: z.literal("current_user_question"),
2054
+ retainTechnicalContext: z.literal(true),
2055
+ productionEnvironmentDisclosure: z.literal("only_when_asked_or_relevant"),
2056
+ nonProductionEnvironmentDisclosure: z.literal("always"),
2044
2057
  translateFields: z.array(z.string()),
2045
- preserveExactFields: z.array(z.string())
2058
+ preserveExactFields: z.array(z.string()),
2059
+ mustTellUser: z.array(z.string()),
2060
+ tellWhenRelevant: z.array(z.string()),
2061
+ agentInstructions: z.array(z.string())
2046
2062
  }).optional(),
2047
2063
  decision: z.object({
2048
2064
  decisionVersion: z.literal(1),
@@ -2097,9 +2113,54 @@ var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
2097
2113
  )
2098
2114
  };
2099
2115
  function structuredToolResult(envelope) {
2116
+ const presentation = {
2117
+ userLanguage: "infer_from_conversation",
2118
+ userTimeZone: "infer_from_conversation",
2119
+ answerScope: "current_user_question",
2120
+ retainTechnicalContext: true,
2121
+ productionEnvironmentDisclosure: "only_when_asked_or_relevant",
2122
+ nonProductionEnvironmentDisclosure: "always",
2123
+ translateFields: [
2124
+ "summary",
2125
+ "userAction.expectedOutcome",
2126
+ "userAction.options[].label",
2127
+ "userAction.options[].expectedOutcome",
2128
+ ...envelope.presentation?.translateFields ?? []
2129
+ ],
2130
+ preserveExactFields: [
2131
+ "data",
2132
+ "nextActions",
2133
+ "operationId",
2134
+ "embedded URLs, paths, IDs, code tokens, tool names, arguments, DNS values, field names, amounts and exact timestamps",
2135
+ ...envelope.presentation?.preserveExactFields ?? []
2136
+ ],
2137
+ mustTellUser: [
2138
+ "the requested result and whether the operation changed anything",
2139
+ "any action the user must personally complete",
2140
+ "material charges, deadlines, cancellation, replacement, credential revocation and other security or irreversible consequences",
2141
+ ...envelope.presentation?.mustTellUser ?? []
2142
+ ],
2143
+ tellWhenRelevant: [
2144
+ "local paths, internal IDs, raw enums, exact arguments, provider state and diagnostic details",
2145
+ "normal Production environment details, unless the user asks or environment identity matters to the task",
2146
+ ...envelope.presentation?.tellWhenRelevant ?? []
2147
+ ],
2148
+ agentInstructions: [
2149
+ "Keep every technical fact available for reasoning and exact continuation; never discard it merely because it is technical.",
2150
+ "Answer the current user question instead of mechanically dumping every returned field. Explain relevant technical terms in plain language while retaining the exact term when it helps accuracy.",
2151
+ "Do not ask the user to copy or run tool arguments that this AI can execute. Use exact nextActions internally after any required user choice.",
2152
+ "Translate human-readable explanations into the language the user is using and present dates and sizes in locally understandable form alongside exact values when precision matters.",
2153
+ "Do not proactively mention a normal Production environment. Always disclose Test or unknown/non-production environments prominently.",
2154
+ ...envelope.presentation?.agentInstructions ?? []
2155
+ ]
2156
+ };
2157
+ const structuredEnvelope = { ...envelope, presentation };
2158
+ const presentationFallback = "AGENT PRESENTATION CONTRACT: Retain every technical fact below for reasoning and exact follow-up. Tell the user the requested result, required next step, and every material charge, deadline, security or irreversible consequence. Answer only the current question instead of dumping all fields; explain relevant technical terms in plain language while keeping exact terms available. Translate human-readable explanations into the user's language, but preserve URLs, paths, IDs, tool names, arguments, DNS values, field names, amounts and exact timestamps. Do not proactively show a normal Production environment; always disclose Test or unknown environments. Never ask the user to run exact tool arguments that this AI can execute.";
2100
2159
  return {
2101
- content: [{ type: "text", text: envelope.summary }],
2102
- structuredContent: envelope
2160
+ content: [{ type: "text", text: `${envelope.summary}
2161
+
2162
+ ${presentationFallback}` }],
2163
+ structuredContent: structuredEnvelope
2103
2164
  };
2104
2165
  }
2105
2166
 
@@ -2250,14 +2311,22 @@ function toolError(e) {
2250
2311
  const timeoutSummary = timedOut ? timeoutRetrySafe ? `${timeoutOperation} exceeded its ${timeoutMs ?? "configured"}ms deadline. Sakupa made zero automatic retries; this was a read, so retry it manually or run help if it repeats.` : `${timeoutOperation} exceeded its ${timeoutMs ?? "configured"}ms deadline. Sakupa made zero automatic retries. The remote outcome may be unknown; do not repeat the write automatically. Query current status or run help first.` : void 0;
2251
2312
  const serverGuidance = isSakupaError(e) && errorCode !== "internal" && errorCode !== "unauthorized" && errorCode !== "upgrade_required" && e.message.trim().length > 0 ? e.message : void 0;
2252
2313
  const safeSummary = timeoutSummary ?? (e instanceof LocalGuidanceError ? e.message : errorCode === "upgrade_required" ? `This Sakupa MCP client is v${MCP_VERSION}, older than the server's minimum supported version${minimumVersion !== void 0 ? ` (v${minimumVersion})` : ""}, so the server refused the call. To fix it: ask the user to fully restart their MCP client session \u2014 "npx -y @sakupa/mcp@latest" setups fetch the current version on restart (run "npx clear-npx-cache" first if the old version persists); global installs need "npm install -g @sakupa/mcp@latest". After the restart, retry this exact tool call.` : errorCode === "unauthorized" ? UNAUTHORIZED_SUMMARY : serverGuidance ?? (retryable ? "An upstream service is temporarily unavailable or busy; retry shortly." : opaqueUnclassified ? "This failed with an error Sakupa could not classify, and retrying the same call will not help. Run help with the failed tool and error code first; only use report if help explicitly recommends it." : "The operation failed; no server-internal details are exposed."));
2314
+ const customerMeaning = timedOut ? timeoutRetrySafe ? "Sakupa did not receive this read result before the deadline; no automatic retry occurred." : "Sakupa did not receive a final result before the deadline, so the AI must check current state before attempting another write." : errorCode === "unauthorized" ? "The cloud site is still intact, but this project no longer has a valid management credential for it." : errorCode === "upgrade_required" ? "The installed Sakupa MCP version is too old for the current API and must be refreshed before retrying." : errorCode === "payment_required" ? "This action needs an active subscription or a payment issue must be resolved first." : errorCode === "rate_limited" ? "Sakupa temporarily refused this operation because a usage or frequency limit was reached." : errorCode === "not_found" ? "The requested Sakupa site, project binding, or operation could not be found." : errorCode === "forbidden" ? "Sakupa refused this operation because the current authority or site state does not allow it." : errorCode === "conflict" || errorCode === "state_conflict" || errorCode === "confirmation_required" ? "Sakupa safely stopped because the site, billing state, or required confirmation no longer matches." : errorCode === "invalid_request" || errorCode === "validation_failed" ? "Sakupa could not complete the operation because required input or current state was invalid." : retryable ? "A temporary Sakupa dependency problem prevented completion." : "Sakupa did not complete the operation; use the retained diagnostics to determine the safe next step.";
2315
+ const userFacingSummary = `Customer meaning: ${customerMeaning}
2316
+ Technical context for the AI: ${safeSummary}`;
2253
2317
  const result = structuredToolResult({
2254
2318
  schemaVersion: 1,
2255
2319
  outcome: "failed",
2256
2320
  resultCode: `error_${errorCode}`,
2257
- summary: safeSummary,
2321
+ summary: userFacingSummary,
2258
2322
  data: {
2259
2323
  errorCode,
2260
2324
  retryable,
2325
+ customerMeaning,
2326
+ diagnosticContext: {
2327
+ technicalMessage: safeSummary,
2328
+ source: serverGuidance ? "sakupa_business_guidance" : "mcp_safe_diagnostic"
2329
+ },
2261
2330
  ...safeDetails && Object.keys(safeDetails).length > 0 ? { details: safeDetails } : {}
2262
2331
  },
2263
2332
  nextActions: [
@@ -3580,16 +3649,11 @@ async function resumeCredentialRotation(client, projectDir, site, apiBaseUrl) {
3580
3649
 
3581
3650
  // src/tools/decision.ts
3582
3651
  var DECISION_PRESENTATION_POLICY = {
3583
- userLanguage: "infer_from_conversation",
3584
3652
  translateFields: [
3585
- "summary",
3586
3653
  "decision.prompt",
3587
3654
  "decision.options[].label",
3588
3655
  "decision.options[].description",
3589
- "decision.options[].consequences[]",
3590
- "userAction.expectedOutcome",
3591
- "userAction.options[].label",
3592
- "userAction.options[].expectedOutcome"
3656
+ "decision.options[].consequences[]"
3593
3657
  ],
3594
3658
  preserveExactFields: [
3595
3659
  "decision.options[].id",
@@ -3597,7 +3661,11 @@ var DECISION_PRESENTATION_POLICY = {
3597
3661
  "userAction.resumeWith",
3598
3662
  "userAction.options[].value",
3599
3663
  "nextActions",
3600
- "embedded URLs, code tokens, field names and confirmation values"
3664
+ "decision option IDs and confirmation values"
3665
+ ],
3666
+ mustTellUser: ["every decision option, with no option selected by default"],
3667
+ agentInstructions: [
3668
+ "Wait for the user's selection, then copy only that option's exact nextAction without reconstructing arguments."
3601
3669
  ]
3602
3670
  };
3603
3671
  var FORBIDDEN_DECISION_ARGUMENT_KEYS = /* @__PURE__ */ new Set([
@@ -3689,9 +3757,7 @@ function formatDecisionFallback(decision) {
3689
3757
  ${option.description}${consequences}
3690
3758
  ${exactAction}`;
3691
3759
  });
3692
- return `LANGUAGE PRESENTATION RULE: Translate all human-readable summaries, prompts, labels, descriptions and consequences into the language the user is using before presenting them. Keep option IDs, tool names, arguments, URLs, field names and confirmation values exactly unchanged.
3693
-
3694
- USER DECISION REQUIRED: ${decision.prompt}
3760
+ return `USER DECISION REQUIRED: ${decision.prompt}
3695
3761
  No option is selected by default. Present every option to the user, do not choose on their behalf, and never reconstruct or guess tool arguments.
3696
3762
 
3697
3763
  ` + options.join("\n\n");
@@ -3800,6 +3866,15 @@ var severityEnum = z2.enum(["low", "medium", "high", "critical"]);
3800
3866
  function planCatalog() {
3801
3867
  return TIER_ORDER.map((p) => `${p} JPY ${tierPriceJpy(p)}/month`).join(", ");
3802
3868
  }
3869
+ function deploymentEnvironmentContext(apiBaseUrl) {
3870
+ const environment = environmentFor(apiBaseUrl);
3871
+ if (environment === "production") {
3872
+ return `Agent context \u2014 environment: PRODUCTION (${apiBaseUrl}). Retain this fact, but do not proactively show it to the user unless asked or relevant.
3873
+ `;
3874
+ }
3875
+ return `IMPORTANT USER NOTICE \u2014 environment: ${environment.toUpperCase()} (${apiBaseUrl}). The user must be told prominently that this is not the normal Production environment.
3876
+ `;
3877
+ }
3803
3878
  var ticketCategoryEnum = z2.enum([
3804
3879
  "billing",
3805
3880
  "payment",
@@ -4050,7 +4125,7 @@ Next action: ${analysis.suggestedNextAction}`,
4050
4125
  server.registerTool(
4051
4126
  "deploy",
4052
4127
  {
4053
- 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://${previewHostPattern}) 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 first and refuses to upload source projects, secrets, .env files, archives, media or server code. The MCP process is locked to the current directory initialized by \`npx -y @sakupa/mcp@latest init\`; no tool argument can change that root. outputDir is a separate REQUIRED relative path supplied from the current project inspection. Never uploads anything when analysis says the project is not deployable.`,
4128
+ 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://${previewHostPattern}) and stores the management credential in .sakupa/site.json. Later runs update the existing site (free sites also refresh their validity; subscription-backed sites have no free-site expiry while the subscription remains active). Runs analyze first and refuses to upload source projects, secrets, .env files, archives, media or server code. The MCP process is locked to the current directory initialized by the no-argument init MCP tool; no tool argument can change that root. outputDir is a separate REQUIRED relative path supplied from the current project inspection. Never uploads anything when analysis says the project is not deployable.`,
4054
4129
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4055
4130
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
4056
4131
  inputSchema: {
@@ -4541,12 +4616,11 @@ Next action: ${analysis.suggestedNextAction}`,
4541
4616
  return text(
4542
4617
  "site_published",
4543
4618
  `Site published: ${finalized2.url}
4544
- Environment: ${environmentFor(ctx.apiBaseUrl).toUpperCase()} (${ctx.apiBaseUrl})
4545
- Project directory: ${ctx.projectDir}
4619
+ ` + deploymentEnvironmentContext(ctx.apiBaseUrl) + `Project directory: ${ctx.projectDir}
4546
4620
  Files uploaded: ${uploaded2} (${finalized2.totalBytes} bytes)
4547
4621
  ` + (finalized2.expiresAt ? `Expires at: ${finalized2.expiresAt}
4548
4622
  ` : "") + `
4549
- This is a FREE temporary preview: it stays live for ${FREE_SITE_TTL_HOURS} hours. Deploying again or calling refresh extends the validity; subscribing the site (subscribe) 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.
4623
+ This is a FREE temporary preview: it stays live for ${FREE_SITE_TTL_HOURS} hours. Deploying again or calling refresh extends the validity; while a subscription remains active, this URL stays live without the free-site expiry. The management credential was saved to .sakupa/site.json \u2014 keep that file: it is the only way to manage this site.
4550
4624
  ` + credentialGitReminder(ctx.projectDir) + (finalized2.warnings.length > 0 ? `
4551
4625
  Warnings:
4552
4626
  ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
@@ -4631,14 +4705,13 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
4631
4705
  return text(
4632
4706
  handoffPerformed ? "free_site_slot_reassigned" : "site_updated",
4633
4707
  `Site updated: ${finalized.url}
4634
- Environment: ${environmentFor(ctx.apiBaseUrl).toUpperCase()} (${ctx.apiBaseUrl})
4635
- Project directory: ${ctx.projectDir}
4708
+ ` + deploymentEnvironmentContext(ctx.apiBaseUrl) + `Project directory: ${ctx.projectDir}
4636
4709
  Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
4637
4710
  ` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
4638
4711
  ` : "") + (credentialRelocatedFrom.length > 0 ? `Credential binding relocated from ${credentialRelocatedFrom.join(", ")} to ${ctx.projectDir}/.sakupa; the existing site was preserved.
4639
4712
  ` : "") + (handoffPerformed ? `Site handoff completed from the authenticated device list. The existing free-site URL stayed the same, the cloud site was NOT deleted, and its content was replaced. Sakupa issued a fresh project credential and revoked ${handoffRevokedCredentials} previous credential(s), so no old project can continue managing this URL.` + (handoffCleanup?.sourceCredentialRemoved ? " A matching obsolete local site.json was removed automatically.\n" : "\n") : "") + (credentialRotationResumed ? "A previously confirmed credential rotation was resumed safely before this deploy; every older credential is revoked.\n" : "") + (finalized.mode === "free" ? `
4640
- Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh call. Subscribing (subscribe) makes the site permanent.
4641
- ` : "\nThis site is subscribed and permanent \u2014 no expiry.\n") + (finalized.warnings.length > 0 ? `
4713
+ Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh call. While a subscription remains active, the site stays live without this free-site expiry.
4714
+ ` : "\nThis site is subscription-backed and has no free-site expiry while the subscription remains active.\n") + (finalized.warnings.length > 0 ? `
4642
4715
  Warnings:
4643
4716
  ${JSON.stringify(finalized.warnings, null, 2)}` : "") + (credentialSecurity?.rotationRecommended ? `
4644
4717
 
@@ -4694,7 +4767,7 @@ Optional security recommendation: this management credential was created at ${cr
4694
4767
  server.registerTool(
4695
4768
  "refresh",
4696
4769
  {
4697
- 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.",
4770
+ description: "Refresh the validity of the free temporary site WITHOUT uploading content. Uses the local credential in .sakupa/site.json. Subscription-backed sites have no free-site expiry while the subscription remains active and need no refresh.",
4698
4771
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4699
4772
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
4700
4773
  inputSchema: {}
@@ -4758,7 +4831,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
4758
4831
  server.registerTool(
4759
4832
  "subscribe",
4760
4833
  {
4761
- description: `Create a Stripe Checkout link that subscribes THIS site to a Sakupa Hosting monthly plan (${planCatalog()}). Paying makes the site PERMANENT on its ${previewHostPattern} URL \u2014 no more 24h expiry; that is the core value of paying. Binding a custom domain afterwards (bind) 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 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.`,
4834
+ description: `Create a Stripe Checkout link that subscribes THIS site to a Sakupa Hosting monthly plan (${planCatalog()}). While the subscription remains active, its ${previewHostPattern} URL stays live without the free 24-hour expiry. Binding a custom domain afterwards (bind) 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 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.`,
4762
4835
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4763
4836
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
4764
4837
  inputSchema: {
@@ -4785,7 +4858,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
4785
4858
  ${res.checkoutUrl}
4786
4859
 
4787
4860
  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.
4788
- Once payment confirms, the site becomes permanent on its current URL. Binding a custom domain (bind) is optional and still requires DNS verification.`,
4861
+ Once Stripe confirms payment and Sakupa synchronizes the subscription, the current URL stays live while that subscription remains active. Binding a custom domain (bind) is optional and still requires DNS verification.`,
4789
4862
  {
4790
4863
  siteId: res.siteId,
4791
4864
  plan: res.plan,
@@ -4804,7 +4877,7 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
4804
4877
  server.registerTool(
4805
4878
  "bind",
4806
4879
  {
4807
- description: `Bind a custom domain to this subscribed site \u2014 an OPTIONAL extra serving surface; the permanent ${previewHostPattern} URL keeps working alongside it. The binding unit is the APEX domain: binding example.com reserves routes for example.com and www.example.com, but ONLY www is required and judged for activation; the naked apex is optional because many DNS providers cannot point it. 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). Ownership is proven ONLY by DNS control of the apex \u2014 payment never grants ownership, and bindings are ALWAYS challengeable: whoever proves CURRENT DNS control takes the domain, even from an existing binding (the displaced site keeps its subscription, content and permanent URL). Unverified requests expire after 72 hours. Call again with action "status" to check progress.`,
4880
+ description: `Bind a custom domain to this subscribed site \u2014 an OPTIONAL extra serving surface; the subscription-backed ${previewHostPattern} URL keeps working alongside it while the subscription is active. The binding unit is the APEX domain: binding example.com reserves routes for example.com and www.example.com, but ONLY www is required and judged for activation; the naked apex is optional because many DNS providers cannot point it. 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). Ownership is proven ONLY by DNS control of the apex \u2014 payment never grants ownership, and bindings are ALWAYS challengeable: whoever proves CURRENT DNS control takes the domain, even from an existing binding (the displaced site keeps its subscription, content and subscription-backed Sakupa URL). Unverified requests expire after 72 hours. Call again with action "status" to check progress.`,
4808
4881
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4809
4882
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
4810
4883
  inputSchema: {
@@ -4932,7 +5005,7 @@ When the user says the TXT is set, run bind "status". It verifies ownership and
4932
5005
  noteSiteMode(res.siteId, res.mode);
4933
5006
  const lines = [
4934
5007
  `AUTHORITATIVE BILLING SNAPSHOT for site ${res.siteId} (mode: ${res.mode})`,
4935
- res.permanentUrl ? `Permanent URL: ${res.permanentUrl}` : void 0,
5008
+ res.permanentUrl ? `Subscription-backed Sakupa URL: ${res.permanentUrl}` : void 0,
4936
5009
  res.plan ? `Current plan: ${res.plan} (JPY ${res.monthlyPriceJpy ?? tierPriceJpy(res.plan)}/month)` : "Current plan: (no subscription yet)",
4937
5010
  res.subscriptionStatus ? `Subscription payment state: ${res.subscriptionStatus}` : void 0,
4938
5011
  res.cancelAtPeriodEnd === true ? `Renewal: CANCELED \u2014 the site reverts to free at ${res.cancellationEffectiveAt ?? res.currentPeriodEnd ?? "the end of the already-paid month"}` : res.renewalPlan ? `Next renewal plan: ${res.renewalPlan} (JPY ${res.renewalMonthlyPriceJpy ?? tierPriceJpy(res.renewalPlan)}/month)` + (res.renewalEffectiveAt ? `, effective ${res.renewalEffectiveAt}` : " (unchanged)") : res.cancelAtPeriodEnd === false ? "Renewal cancellation: no" : void 0,
@@ -5634,7 +5707,7 @@ var TOOL_MANUALS = {
5634
5707
  init: {
5635
5708
  purpose: "Initialize the active IDE workspace as one Sakupa project.",
5636
5709
  sideEffects: "Creates only .sakupa/project.json locally; no API call, site or charge.",
5637
- preconditions: "Exactly one usable MCP workspace Root. Clients without Roots use CLI init.",
5710
+ preconditions: "Exactly one usable MCP workspace Root. If Roots are unavailable, help may authorize the AI to use CLI init.",
5638
5711
  parameterNames: [],
5639
5712
  parameters: "No parameters and no path argument.",
5640
5713
  warnings: [
@@ -5683,7 +5756,9 @@ var TOOL_MANUALS = {
5683
5756
  preconditions: "A valid local site credential.",
5684
5757
  parameterNames: [],
5685
5758
  parameters: "No parameters.",
5686
- warnings: ["Subscribed sites are permanent and do not need refresh."],
5759
+ warnings: [
5760
+ "Subscription-backed sites have no free-site expiry while the subscription remains active and do not need refresh."
5761
+ ],
5687
5762
  nextStep: "Call status to verify the new expiry."
5688
5763
  },
5689
5764
  status: {
@@ -5825,7 +5900,7 @@ function registerHelpTools(server, baseCtx) {
5825
5900
  server.registerTool(
5826
5901
  "init",
5827
5902
  {
5828
- description: "Initialize the active MCP workspace Root as a Sakupa project. Takes no path argument, creates only .sakupa/project.json at that exact Root, preserves site/recovery state, makes no API call and is idempotent. Clients without MCP Roots must use the no-argument CLI init.",
5903
+ description: "Initialize the active MCP workspace Root as a Sakupa project. Takes no path argument, creates only .sakupa/project.json at that exact Root, preserves site/recovery state, makes no API call and is idempotent. If MCP Roots are unavailable, call help; the AI may then use the no-argument CLI init itself.",
5829
5904
  inputSchema: {},
5830
5905
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5831
5906
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
@@ -6151,14 +6226,14 @@ Workflow:
6151
6226
  current deployment and serving state at any time. Every update checks the credential's
6152
6227
  server-side issue time. When it is older than 7 days, deploy succeeds and only SUGGESTS the
6153
6228
  optional rotate tool; never rotate without the user's explicit confirmation.
6154
- 3. To make the site PERMANENT, subscribe it to a monthly hosting plan (plans
6229
+ 3. To keep the site live beyond the free period, subscribe it to a monthly hosting plan (plans
6155
6230
  shows the catalog; subscribe -> Stripe-hosted checkout;
6156
- water/personal/share/business). Paying makes the
6157
- ${hostPattern} URL permanent \u2014 that is what payment buys. Usage over the chosen plan
6231
+ water/personal/share/business). While the subscription remains active, the
6232
+ ${hostPattern} URL stays live without the free 24-hour expiry. Usage over the chosen plan
6158
6233
  shows an over-limit notice by default. An external AI may periodically query usage and
6159
6234
  recommend a plan, but Sakupa never changes a subscription automatically.
6160
6235
  4. Optionally bind a custom domain to the subscribed site (bind): an included extra
6161
- serving surface alongside the permanent URL. Ownership is proven only by DNS control; the
6236
+ serving surface alongside the subscription-backed Sakupa URL. Ownership is proven only by DNS control; the
6162
6237
  first verified request wins; unverified requests expire after 72 hours. billing,
6163
6238
  change, portal and recover manage the paid lifecycle. Binding a
6164
6239
  NEW domain while one is live is a zero-downtime SWITCH: the old domain keeps serving
@@ -6183,11 +6258,12 @@ translate every human-readable summary, prompt, option label, description and co
6183
6258
  language. Keep option IDs, tool names, exact arguments, URLs, field names and confirmation values
6184
6259
  unchanged.
6185
6260
 
6186
- Project directory contract: before the first deploy or a new recovery, initialize the intended
6187
- project by calling init with NO path argument. init uses the IDE's exact MCP Root and creates the
6188
- non-secret .sakupa/project.json directly there. The CLI command
6189
- "npx -y @sakupa/mcp@latest init" remains the safe fallback for clients without MCP Roots and
6190
- also accepts NO path argument. ONE MCP process = ONE Roots-first locked project = ONE site.
6261
+ Project directory contract: before the first deploy or a new recovery, CALL the init MCP tool with
6262
+ NO path argument. init uses the IDE's exact MCP Root and creates the non-secret
6263
+ .sakupa/project.json directly there. Do not merely print installation or CLI instructions when the
6264
+ init tool is available. Only after help confirms that the client does not provide MCP Roots may the
6265
+ AI itself use the CLI command "npx -y @sakupa/mcp@latest init" as a fallback; never ask the user to
6266
+ run it. The CLI also accepts NO path argument. ONE MCP process = ONE Roots-first locked project = ONE site.
6191
6267
  Site tools do not accept projectDir and cannot select another root; help, plans and report preview
6192
6268
  and public_recovery portal remain project-independent.
6193
6269
  Sakupa stores .sakupa/site.json and recovery state only in the locked directory; it never uses
@@ -6200,11 +6276,21 @@ before retrying with outputDirChangeConfirmed: true.
6200
6276
  After init, require sakupaAtProjectRoot=true. Before deploy, Sakupa checks every directory segment
6201
6277
  between the project Root and outputDir for a misplaced .sakupa and safely relocates only validated,
6202
6278
  non-conflicting state; never copy, delete or overwrite site.json by shell command.
6203
- After every deploy, TELL the user which environment it went to (deploy results carry an
6204
- Explicit Environment line: TEST vs PRODUCTION). analyze, deploy, status,
6205
- and refresh echo
6279
+ Every result retains the exact environment for AI reasoning. Prominently tell the user when the
6280
+ environment is TEST or unknown/non-production. Do not proactively mention a normal PRODUCTION
6281
+ environment unless the user asks or the distinction is relevant to the current question. analyze,
6282
+ deploy, status, and refresh echo
6206
6283
  the Roots-first locked directory they acted on.
6207
6284
 
6285
+ Presentation contract: preserve every returned technical fact for reasoning and exact continuation,
6286
+ including paths, IDs, enums, tool names, arguments, DNS values, provider states, amounts and exact
6287
+ timestamps. Do not mechanically dump every field. Answer the user's current question first, explain
6288
+ relevant technical terms in plain language while retaining the exact term, translate human-readable
6289
+ explanations into the user's language, and always disclose material charges, deadlines, cancellation,
6290
+ replacement, credential revocation, security effects and irreversible consequences. A technical fact
6291
+ may be omitted from the immediate user-facing answer only when it is irrelevant to the current
6292
+ question; it must remain available in the tool result for follow-up.
6293
+
6208
6294
  On any difficulty, call help before retrying or escalating. Only offer report when help returns
6209
6295
  reportRecommended:true; attach your own factual account via agentContext and show the exact
6210
6296
  sanitized preview before asking the user to confirm submission.
@@ -6288,10 +6374,12 @@ export {
6288
6374
  FetchTransport,
6289
6375
  HttpApiClient,
6290
6376
  MCP_VERSION,
6377
+ PRODUCTION_API_BASE_URL,
6291
6378
  TEST_API_BASE_URL,
6292
6379
  analyzeProject,
6293
6380
  createSakupaMcpServer,
6294
6381
  deleteSiteFile,
6382
+ environmentFor,
6295
6383
  loadMcpRuntimeConfig,
6296
6384
  loadSiteFile,
6297
6385
  registerTools,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakupa/mcp",
3
- "version": "0.7.48",
3
+ "version": "0.7.49",
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",