@sakupa/mcp 0.7.47 → 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 +154 -36
  2. package/dist/index.js +156 -36
  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.47";
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
  }
@@ -3229,6 +3237,19 @@ var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
3229
3237
  operationId: z.string().optional(),
3230
3238
  summary: z.string(),
3231
3239
  data: z.record(z.string(), z.unknown()),
3240
+ presentation: z.object({
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"),
3247
+ translateFields: 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())
3252
+ }).optional(),
3232
3253
  decision: z.object({
3233
3254
  decisionVersion: z.literal(1),
3234
3255
  prompt: z.string(),
@@ -3282,9 +3303,54 @@ var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
3282
3303
  )
3283
3304
  };
3284
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.";
3285
3349
  return {
3286
- content: [{ type: "text", text: envelope.summary }],
3287
- structuredContent: envelope
3350
+ content: [{ type: "text", text: `${envelope.summary}
3351
+
3352
+ ${presentationFallback}` }],
3353
+ structuredContent: structuredEnvelope
3288
3354
  };
3289
3355
  }
3290
3356
 
@@ -3435,14 +3501,22 @@ function toolError(e) {
3435
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;
3436
3502
  const serverGuidance = isSakupaError(e) && errorCode !== "internal" && errorCode !== "unauthorized" && errorCode !== "upgrade_required" && e.message.trim().length > 0 ? e.message : void 0;
3437
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}`;
3438
3507
  const result = structuredToolResult({
3439
3508
  schemaVersion: 1,
3440
3509
  outcome: "failed",
3441
3510
  resultCode: `error_${errorCode}`,
3442
- summary: safeSummary,
3511
+ summary: userFacingSummary,
3443
3512
  data: {
3444
3513
  errorCode,
3445
3514
  retryable,
3515
+ customerMeaning,
3516
+ diagnosticContext: {
3517
+ technicalMessage: safeSummary,
3518
+ source: serverGuidance ? "sakupa_business_guidance" : "mcp_safe_diagnostic"
3519
+ },
3446
3520
  ...safeDetails && Object.keys(safeDetails).length > 0 ? { details: safeDetails } : {}
3447
3521
  },
3448
3522
  nextActions: [
@@ -3458,6 +3532,26 @@ function toolError(e) {
3458
3532
  }
3459
3533
 
3460
3534
  // src/tools/decision.ts
3535
+ var DECISION_PRESENTATION_POLICY = {
3536
+ translateFields: [
3537
+ "decision.prompt",
3538
+ "decision.options[].label",
3539
+ "decision.options[].description",
3540
+ "decision.options[].consequences[]"
3541
+ ],
3542
+ preserveExactFields: [
3543
+ "decision.options[].id",
3544
+ "decision.options[].nextAction",
3545
+ "userAction.resumeWith",
3546
+ "userAction.options[].value",
3547
+ "nextActions",
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."
3553
+ ]
3554
+ };
3461
3555
  var FORBIDDEN_DECISION_ARGUMENT_KEYS = /* @__PURE__ */ new Set([
3462
3556
  "credential",
3463
3557
  "candidatecredential",
@@ -3598,6 +3692,7 @@ function decisionToolResult(input) {
3598
3692
 
3599
3693
  ${formatDecisionFallback(decision)}`,
3600
3694
  data: input.data,
3695
+ presentation: DECISION_PRESENTATION_POLICY,
3601
3696
  decision,
3602
3697
  ...userAction === void 0 ? {} : { userAction },
3603
3698
  nextActions
@@ -3655,6 +3750,15 @@ var severityEnum = z2.enum(["low", "medium", "high", "critical"]);
3655
3750
  function planCatalog() {
3656
3751
  return TIER_ORDER.map((p) => `${p} JPY ${tierPriceJpy(p)}/month`).join(", ");
3657
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
+ }
3658
3762
  var ticketCategoryEnum = z2.enum([
3659
3763
  "billing",
3660
3764
  "payment",
@@ -3905,7 +4009,7 @@ Next action: ${analysis.suggestedNextAction}`,
3905
4009
  server.registerTool(
3906
4010
  "deploy",
3907
4011
  {
3908
- 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.`,
3909
4013
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
3910
4014
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
3911
4015
  inputSchema: {
@@ -4396,12 +4500,11 @@ Next action: ${analysis.suggestedNextAction}`,
4396
4500
  return text(
4397
4501
  "site_published",
4398
4502
  `Site published: ${finalized2.url}
4399
- Environment: ${environmentFor(ctx.apiBaseUrl).toUpperCase()} (${ctx.apiBaseUrl})
4400
- Project directory: ${ctx.projectDir}
4503
+ ` + deploymentEnvironmentContext(ctx.apiBaseUrl) + `Project directory: ${ctx.projectDir}
4401
4504
  Files uploaded: ${uploaded2} (${finalized2.totalBytes} bytes)
4402
4505
  ` + (finalized2.expiresAt ? `Expires at: ${finalized2.expiresAt}
4403
4506
  ` : "") + `
4404
- 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.
4405
4508
  ` + credentialGitReminder(ctx.projectDir) + (finalized2.warnings.length > 0 ? `
4406
4509
  Warnings:
4407
4510
  ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
@@ -4486,14 +4589,13 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
4486
4589
  return text(
4487
4590
  handoffPerformed ? "free_site_slot_reassigned" : "site_updated",
4488
4591
  `Site updated: ${finalized.url}
4489
- Environment: ${environmentFor(ctx.apiBaseUrl).toUpperCase()} (${ctx.apiBaseUrl})
4490
- Project directory: ${ctx.projectDir}
4592
+ ` + deploymentEnvironmentContext(ctx.apiBaseUrl) + `Project directory: ${ctx.projectDir}
4491
4593
  Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
4492
4594
  ` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
4493
4595
  ` : "") + (credentialRelocatedFrom.length > 0 ? `Credential binding relocated from ${credentialRelocatedFrom.join(", ")} to ${ctx.projectDir}/.sakupa; the existing site was preserved.
4494
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" ? `
4495
- Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh call. Subscribing (subscribe) makes the site permanent.
4496
- ` : "\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 ? `
4497
4599
  Warnings:
4498
4600
  ${JSON.stringify(finalized.warnings, null, 2)}` : "") + (credentialSecurity?.rotationRecommended ? `
4499
4601
 
@@ -4549,7 +4651,7 @@ Optional security recommendation: this management credential was created at ${cr
4549
4651
  server.registerTool(
4550
4652
  "refresh",
4551
4653
  {
4552
- 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.",
4553
4655
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4554
4656
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
4555
4657
  inputSchema: {}
@@ -4613,7 +4715,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
4613
4715
  server.registerTool(
4614
4716
  "subscribe",
4615
4717
  {
4616
- 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.`,
4617
4719
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4618
4720
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
4619
4721
  inputSchema: {
@@ -4640,7 +4742,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
4640
4742
  ${res.checkoutUrl}
4641
4743
 
4642
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.
4643
- 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.`,
4644
4746
  {
4645
4747
  siteId: res.siteId,
4646
4748
  plan: res.plan,
@@ -4659,7 +4761,7 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
4659
4761
  server.registerTool(
4660
4762
  "bind",
4661
4763
  {
4662
- 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.`,
4663
4765
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4664
4766
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
4665
4767
  inputSchema: {
@@ -4787,7 +4889,7 @@ When the user says the TXT is set, run bind "status". It verifies ownership and
4787
4889
  noteSiteMode(res.siteId, res.mode);
4788
4890
  const lines = [
4789
4891
  `AUTHORITATIVE BILLING SNAPSHOT for site ${res.siteId} (mode: ${res.mode})`,
4790
- res.permanentUrl ? `Permanent URL: ${res.permanentUrl}` : void 0,
4892
+ res.permanentUrl ? `Subscription-backed Sakupa URL: ${res.permanentUrl}` : void 0,
4791
4893
  res.plan ? `Current plan: ${res.plan} (JPY ${res.monthlyPriceJpy ?? tierPriceJpy(res.plan)}/month)` : "Current plan: (no subscription yet)",
4792
4894
  res.subscriptionStatus ? `Subscription payment state: ${res.subscriptionStatus}` : void 0,
4793
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,
@@ -5486,7 +5588,7 @@ var TOOL_MANUALS = {
5486
5588
  init: {
5487
5589
  purpose: "Initialize the active IDE workspace as one Sakupa project.",
5488
5590
  sideEffects: "Creates only .sakupa/project.json locally; no API call, site or charge.",
5489
- 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.",
5490
5592
  parameterNames: [],
5491
5593
  parameters: "No parameters and no path argument.",
5492
5594
  warnings: [
@@ -5535,7 +5637,9 @@ var TOOL_MANUALS = {
5535
5637
  preconditions: "A valid local site credential.",
5536
5638
  parameterNames: [],
5537
5639
  parameters: "No parameters.",
5538
- 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
+ ],
5539
5643
  nextStep: "Call status to verify the new expiry."
5540
5644
  },
5541
5645
  status: {
@@ -5677,7 +5781,7 @@ function registerHelpTools(server, baseCtx) {
5677
5781
  server.registerTool(
5678
5782
  "init",
5679
5783
  {
5680
- 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.",
5681
5785
  inputSchema: {},
5682
5786
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5683
5787
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
@@ -6154,14 +6258,14 @@ Workflow:
6154
6258
  current deployment and serving state at any time. Every update checks the credential's
6155
6259
  server-side issue time. When it is older than 7 days, deploy succeeds and only SUGGESTS the
6156
6260
  optional rotate tool; never rotate without the user's explicit confirmation.
6157
- 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
6158
6262
  shows the catalog; subscribe -> Stripe-hosted checkout;
6159
- water/personal/share/business). Paying makes the
6160
- ${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
6161
6265
  shows an over-limit notice by default. An external AI may periodically query usage and
6162
6266
  recommend a plan, but Sakupa never changes a subscription automatically.
6163
6267
  4. Optionally bind a custom domain to the subscribed site (bind): an included extra
6164
- 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
6165
6269
  first verified request wins; unverified requests expire after 72 hours. billing,
6166
6270
  change, portal and recover manage the paid lifecycle. Binding a
6167
6271
  NEW domain while one is live is a zero-downtime SWITCH: the old domain keeps serving
@@ -6181,13 +6285,17 @@ context, paraphrase a selection into different arguments, or invent another opti
6181
6285
  selects, copy that option's exact nextAction. Legacy userAction and nextActions mirror the same
6182
6286
  choice for older clients; decision is the authoritative choice set. A type "none" option means call
6183
6287
  no tool and change nothing. Stripe-hosted links remain direct links because Stripe itself owns plan
6184
- selection and confirmation.
6288
+ selection and confirmation. Before showing any decision, infer the language the user is using and
6289
+ translate every human-readable summary, prompt, option label, description and consequence into that
6290
+ language. Keep option IDs, tool names, exact arguments, URLs, field names and confirmation values
6291
+ unchanged.
6185
6292
 
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.
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.
6191
6299
  Site tools do not accept projectDir and cannot select another root; help, plans and report preview
6192
6300
  and public_recovery portal remain project-independent.
6193
6301
  Sakupa stores .sakupa/site.json and recovery state only in the locked directory; it never uses
@@ -6200,11 +6308,21 @@ before retrying with outputDirChangeConfirmed: true.
6200
6308
  After init, require sakupaAtProjectRoot=true. Before deploy, Sakupa checks every directory segment
6201
6309
  between the project Root and outputDir for a misplaced .sakupa and safely relocates only validated,
6202
6310
  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
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
6206
6315
  the Roots-first locked directory they acted on.
6207
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
+
6208
6326
  On any difficulty, call help before retrying or escalating. Only offer report when help returns
6209
6327
  reportRecommended:true; attach your own factual account via agentContext and show the exact
6210
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.47";
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
  }
@@ -2039,6 +2047,19 @@ var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
2039
2047
  operationId: z.string().optional(),
2040
2048
  summary: z.string(),
2041
2049
  data: z.record(z.string(), z.unknown()),
2050
+ presentation: z.object({
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"),
2057
+ translateFields: 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())
2062
+ }).optional(),
2042
2063
  decision: z.object({
2043
2064
  decisionVersion: z.literal(1),
2044
2065
  prompt: z.string(),
@@ -2092,9 +2113,54 @@ var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
2092
2113
  )
2093
2114
  };
2094
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.";
2095
2159
  return {
2096
- content: [{ type: "text", text: envelope.summary }],
2097
- structuredContent: envelope
2160
+ content: [{ type: "text", text: `${envelope.summary}
2161
+
2162
+ ${presentationFallback}` }],
2163
+ structuredContent: structuredEnvelope
2098
2164
  };
2099
2165
  }
2100
2166
 
@@ -2245,14 +2311,22 @@ function toolError(e) {
2245
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;
2246
2312
  const serverGuidance = isSakupaError(e) && errorCode !== "internal" && errorCode !== "unauthorized" && errorCode !== "upgrade_required" && e.message.trim().length > 0 ? e.message : void 0;
2247
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}`;
2248
2317
  const result = structuredToolResult({
2249
2318
  schemaVersion: 1,
2250
2319
  outcome: "failed",
2251
2320
  resultCode: `error_${errorCode}`,
2252
- summary: safeSummary,
2321
+ summary: userFacingSummary,
2253
2322
  data: {
2254
2323
  errorCode,
2255
2324
  retryable,
2325
+ customerMeaning,
2326
+ diagnosticContext: {
2327
+ technicalMessage: safeSummary,
2328
+ source: serverGuidance ? "sakupa_business_guidance" : "mcp_safe_diagnostic"
2329
+ },
2256
2330
  ...safeDetails && Object.keys(safeDetails).length > 0 ? { details: safeDetails } : {}
2257
2331
  },
2258
2332
  nextActions: [
@@ -3574,6 +3648,26 @@ async function resumeCredentialRotation(client, projectDir, site, apiBaseUrl) {
3574
3648
  }
3575
3649
 
3576
3650
  // src/tools/decision.ts
3651
+ var DECISION_PRESENTATION_POLICY = {
3652
+ translateFields: [
3653
+ "decision.prompt",
3654
+ "decision.options[].label",
3655
+ "decision.options[].description",
3656
+ "decision.options[].consequences[]"
3657
+ ],
3658
+ preserveExactFields: [
3659
+ "decision.options[].id",
3660
+ "decision.options[].nextAction",
3661
+ "userAction.resumeWith",
3662
+ "userAction.options[].value",
3663
+ "nextActions",
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."
3669
+ ]
3670
+ };
3577
3671
  var FORBIDDEN_DECISION_ARGUMENT_KEYS = /* @__PURE__ */ new Set([
3578
3672
  "credential",
3579
3673
  "candidatecredential",
@@ -3714,6 +3808,7 @@ function decisionToolResult(input) {
3714
3808
 
3715
3809
  ${formatDecisionFallback(decision)}`,
3716
3810
  data: input.data,
3811
+ presentation: DECISION_PRESENTATION_POLICY,
3717
3812
  decision,
3718
3813
  ...userAction === void 0 ? {} : { userAction },
3719
3814
  nextActions
@@ -3771,6 +3866,15 @@ var severityEnum = z2.enum(["low", "medium", "high", "critical"]);
3771
3866
  function planCatalog() {
3772
3867
  return TIER_ORDER.map((p) => `${p} JPY ${tierPriceJpy(p)}/month`).join(", ");
3773
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
+ }
3774
3878
  var ticketCategoryEnum = z2.enum([
3775
3879
  "billing",
3776
3880
  "payment",
@@ -4021,7 +4125,7 @@ Next action: ${analysis.suggestedNextAction}`,
4021
4125
  server.registerTool(
4022
4126
  "deploy",
4023
4127
  {
4024
- 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.`,
4025
4129
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4026
4130
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
4027
4131
  inputSchema: {
@@ -4512,12 +4616,11 @@ Next action: ${analysis.suggestedNextAction}`,
4512
4616
  return text(
4513
4617
  "site_published",
4514
4618
  `Site published: ${finalized2.url}
4515
- Environment: ${environmentFor(ctx.apiBaseUrl).toUpperCase()} (${ctx.apiBaseUrl})
4516
- Project directory: ${ctx.projectDir}
4619
+ ` + deploymentEnvironmentContext(ctx.apiBaseUrl) + `Project directory: ${ctx.projectDir}
4517
4620
  Files uploaded: ${uploaded2} (${finalized2.totalBytes} bytes)
4518
4621
  ` + (finalized2.expiresAt ? `Expires at: ${finalized2.expiresAt}
4519
4622
  ` : "") + `
4520
- 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.
4521
4624
  ` + credentialGitReminder(ctx.projectDir) + (finalized2.warnings.length > 0 ? `
4522
4625
  Warnings:
4523
4626
  ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
@@ -4602,14 +4705,13 @@ ${JSON.stringify(finalized2.warnings, null, 2)}` : ""),
4602
4705
  return text(
4603
4706
  handoffPerformed ? "free_site_slot_reassigned" : "site_updated",
4604
4707
  `Site updated: ${finalized.url}
4605
- Environment: ${environmentFor(ctx.apiBaseUrl).toUpperCase()} (${ctx.apiBaseUrl})
4606
- Project directory: ${ctx.projectDir}
4708
+ ` + deploymentEnvironmentContext(ctx.apiBaseUrl) + `Project directory: ${ctx.projectDir}
4607
4709
  Files uploaded: ${uploaded} (${finalized.totalBytes} bytes)
4608
4710
  ` + (finalized.expiresAt ? `Validity refreshed \u2014 expires at: ${finalized.expiresAt}
4609
4711
  ` : "") + (credentialRelocatedFrom.length > 0 ? `Credential binding relocated from ${credentialRelocatedFrom.join(", ")} to ${ctx.projectDir}/.sakupa; the existing site was preserved.
4610
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" ? `
4611
- Reminder: free sites stay live for ${FREE_SITE_TTL_HOURS} hours after the last deploy or refresh call. Subscribing (subscribe) makes the site permanent.
4612
- ` : "\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 ? `
4613
4715
  Warnings:
4614
4716
  ${JSON.stringify(finalized.warnings, null, 2)}` : "") + (credentialSecurity?.rotationRecommended ? `
4615
4717
 
@@ -4665,7 +4767,7 @@ Optional security recommendation: this management credential was created at ${cr
4665
4767
  server.registerTool(
4666
4768
  "refresh",
4667
4769
  {
4668
- 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.",
4669
4771
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4670
4772
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
4671
4773
  inputSchema: {}
@@ -4729,7 +4831,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
4729
4831
  server.registerTool(
4730
4832
  "subscribe",
4731
4833
  {
4732
- 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.`,
4733
4835
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4734
4836
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
4735
4837
  inputSchema: {
@@ -4756,7 +4858,7 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
4756
4858
  ${res.checkoutUrl}
4757
4859
 
4758
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.
4759
- 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.`,
4760
4862
  {
4761
4863
  siteId: res.siteId,
4762
4864
  plan: res.plan,
@@ -4775,7 +4877,7 @@ Once payment confirms, the site becomes permanent on its current URL. Binding a
4775
4877
  server.registerTool(
4776
4878
  "bind",
4777
4879
  {
4778
- 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.`,
4779
4881
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4780
4882
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
4781
4883
  inputSchema: {
@@ -4903,7 +5005,7 @@ When the user says the TXT is set, run bind "status". It verifies ownership and
4903
5005
  noteSiteMode(res.siteId, res.mode);
4904
5006
  const lines = [
4905
5007
  `AUTHORITATIVE BILLING SNAPSHOT for site ${res.siteId} (mode: ${res.mode})`,
4906
- res.permanentUrl ? `Permanent URL: ${res.permanentUrl}` : void 0,
5008
+ res.permanentUrl ? `Subscription-backed Sakupa URL: ${res.permanentUrl}` : void 0,
4907
5009
  res.plan ? `Current plan: ${res.plan} (JPY ${res.monthlyPriceJpy ?? tierPriceJpy(res.plan)}/month)` : "Current plan: (no subscription yet)",
4908
5010
  res.subscriptionStatus ? `Subscription payment state: ${res.subscriptionStatus}` : void 0,
4909
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,
@@ -5605,7 +5707,7 @@ var TOOL_MANUALS = {
5605
5707
  init: {
5606
5708
  purpose: "Initialize the active IDE workspace as one Sakupa project.",
5607
5709
  sideEffects: "Creates only .sakupa/project.json locally; no API call, site or charge.",
5608
- 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.",
5609
5711
  parameterNames: [],
5610
5712
  parameters: "No parameters and no path argument.",
5611
5713
  warnings: [
@@ -5654,7 +5756,9 @@ var TOOL_MANUALS = {
5654
5756
  preconditions: "A valid local site credential.",
5655
5757
  parameterNames: [],
5656
5758
  parameters: "No parameters.",
5657
- 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
+ ],
5658
5762
  nextStep: "Call status to verify the new expiry."
5659
5763
  },
5660
5764
  status: {
@@ -5796,7 +5900,7 @@ function registerHelpTools(server, baseCtx) {
5796
5900
  server.registerTool(
5797
5901
  "init",
5798
5902
  {
5799
- 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.",
5800
5904
  inputSchema: {},
5801
5905
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5802
5906
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
@@ -6122,14 +6226,14 @@ Workflow:
6122
6226
  current deployment and serving state at any time. Every update checks the credential's
6123
6227
  server-side issue time. When it is older than 7 days, deploy succeeds and only SUGGESTS the
6124
6228
  optional rotate tool; never rotate without the user's explicit confirmation.
6125
- 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
6126
6230
  shows the catalog; subscribe -> Stripe-hosted checkout;
6127
- water/personal/share/business). Paying makes the
6128
- ${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
6129
6233
  shows an over-limit notice by default. An external AI may periodically query usage and
6130
6234
  recommend a plan, but Sakupa never changes a subscription automatically.
6131
6235
  4. Optionally bind a custom domain to the subscribed site (bind): an included extra
6132
- 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
6133
6237
  first verified request wins; unverified requests expire after 72 hours. billing,
6134
6238
  change, portal and recover manage the paid lifecycle. Binding a
6135
6239
  NEW domain while one is live is a zero-downtime SWITCH: the old domain keeps serving
@@ -6149,13 +6253,17 @@ context, paraphrase a selection into different arguments, or invent another opti
6149
6253
  selects, copy that option's exact nextAction. Legacy userAction and nextActions mirror the same
6150
6254
  choice for older clients; decision is the authoritative choice set. A type "none" option means call
6151
6255
  no tool and change nothing. Stripe-hosted links remain direct links because Stripe itself owns plan
6152
- selection and confirmation.
6256
+ selection and confirmation. Before showing any decision, infer the language the user is using and
6257
+ translate every human-readable summary, prompt, option label, description and consequence into that
6258
+ language. Keep option IDs, tool names, exact arguments, URLs, field names and confirmation values
6259
+ unchanged.
6153
6260
 
6154
- Project directory contract: before the first deploy or a new recovery, initialize the intended
6155
- project by calling init with NO path argument. init uses the IDE's exact MCP Root and creates the
6156
- non-secret .sakupa/project.json directly there. The CLI command
6157
- "npx -y @sakupa/mcp@latest init" remains the safe fallback for clients without MCP Roots and
6158
- 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.
6159
6267
  Site tools do not accept projectDir and cannot select another root; help, plans and report preview
6160
6268
  and public_recovery portal remain project-independent.
6161
6269
  Sakupa stores .sakupa/site.json and recovery state only in the locked directory; it never uses
@@ -6168,11 +6276,21 @@ before retrying with outputDirChangeConfirmed: true.
6168
6276
  After init, require sakupaAtProjectRoot=true. Before deploy, Sakupa checks every directory segment
6169
6277
  between the project Root and outputDir for a misplaced .sakupa and safely relocates only validated,
6170
6278
  non-conflicting state; never copy, delete or overwrite site.json by shell command.
6171
- After every deploy, TELL the user which environment it went to (deploy results carry an
6172
- Explicit Environment line: TEST vs PRODUCTION). analyze, deploy, status,
6173
- 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
6174
6283
  the Roots-first locked directory they acted on.
6175
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
+
6176
6294
  On any difficulty, call help before retrying or escalating. Only offer report when help returns
6177
6295
  reportRecommended:true; attach your own factual account via agentContext and show the exact
6178
6296
  sanitized preview before asking the user to confirm submission.
@@ -6256,10 +6374,12 @@ export {
6256
6374
  FetchTransport,
6257
6375
  HttpApiClient,
6258
6376
  MCP_VERSION,
6377
+ PRODUCTION_API_BASE_URL,
6259
6378
  TEST_API_BASE_URL,
6260
6379
  analyzeProject,
6261
6380
  createSakupaMcpServer,
6262
6381
  deleteSiteFile,
6382
+ environmentFor,
6263
6383
  loadMcpRuntimeConfig,
6264
6384
  loadSiteFile,
6265
6385
  registerTools,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakupa/mcp",
3
- "version": "0.7.47",
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",