@sakupa/mcp 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/bin.js +149 -104
  2. package/dist/index.js +138 -98
  3. package/package.json +15 -3
package/dist/bin.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/bin.ts
4
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
5
5
  import { stdout } from "node:process";
6
6
 
7
7
  // src/project-root.ts
@@ -402,7 +402,7 @@ function isFreeSiteAllowanceNetworkReference(value) {
402
402
  }
403
403
 
404
404
  // ../core/dist/domain/version.js
405
- var SAKUPA_MCP_VERSION = "1.0.0";
405
+ var SAKUPA_MCP_VERSION = "1.1.0";
406
406
 
407
407
  // ../core/dist/domain/errors.js
408
408
  var HTTP_STATUS = {
@@ -765,7 +765,11 @@ function environmentFor(apiBaseUrl) {
765
765
  }
766
766
 
767
767
  // src/server.ts
768
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
768
+ import {
769
+ CLIENT_CAPABILITIES_META_KEY,
770
+ McpServer,
771
+ inputResponse
772
+ } from "@modelcontextprotocol/server";
769
773
 
770
774
  // src/api-client.ts
771
775
  var KNOWN_ERROR_CODES = /* @__PURE__ */ new Set([
@@ -2946,10 +2950,21 @@ async function resumeCredentialRotation(client, projectDir, site, apiBaseUrl) {
2946
2950
  };
2947
2951
  }
2948
2952
 
2953
+ // src/tools/context.ts
2954
+ import {
2955
+ inputRequired
2956
+ } from "@modelcontextprotocol/server";
2957
+
2949
2958
  // src/project-binding.ts
2950
2959
  import { fileURLToPath } from "node:url";
2951
2960
  import { resolve as resolve4 } from "node:path";
2952
2961
  var MCP_ROOTS_TIMEOUT_MS = 5e3;
2962
+ var McpRootsPending = class extends Error {
2963
+ constructor() {
2964
+ super("MCP Roots must be requested from the client before the project can be bound.");
2965
+ this.name = "McpRootsPending";
2966
+ }
2967
+ };
2953
2968
  var ProjectBindingError = class extends Error {
2954
2969
  diagnostics;
2955
2970
  constructor(diagnostics) {
@@ -2967,10 +2982,10 @@ var ProjectBindingResolver = class {
2967
2982
  bound;
2968
2983
  boundState;
2969
2984
  resolving;
2970
- async resolve() {
2985
+ async resolve(call) {
2971
2986
  if (this.bound) return this.bound;
2972
2987
  if (this.resolving) return this.resolving;
2973
- this.resolving = this.inspect().then((inspection) => {
2988
+ this.resolving = this.inspect(false, call).then((inspection) => {
2974
2989
  if (!inspection.selected) throw new ProjectBindingError(inspection.diagnostics);
2975
2990
  this.bound = inspection.selected;
2976
2991
  this.boundState = inspection.diagnostics;
@@ -2980,18 +2995,18 @@ var ProjectBindingResolver = class {
2980
2995
  });
2981
2996
  return this.resolving;
2982
2997
  }
2983
- async diagnose() {
2998
+ async diagnose(call) {
2984
2999
  if (this.bound) return this.boundState ?? boundDiagnostics(this.processCwd, this.bound);
2985
- const inspection = await this.inspect();
3000
+ const inspection = await this.inspect(false, call);
2986
3001
  if (inspection.selected) {
2987
3002
  this.bound = inspection.selected;
2988
3003
  this.boundState = inspection.diagnostics;
2989
3004
  }
2990
3005
  return inspection.diagnostics;
2991
3006
  }
2992
- async initialize() {
3007
+ async initialize(call) {
2993
3008
  if (this.bound) return this.bound;
2994
- const inspection = await this.inspect(true);
3009
+ const inspection = await this.inspect(true, call);
2995
3010
  if (inspection.selected) {
2996
3011
  this.bound = inspection.selected;
2997
3012
  this.boundState = inspection.diagnostics;
@@ -3010,8 +3025,8 @@ var ProjectBindingResolver = class {
3010
3025
  );
3011
3026
  return this.bound;
3012
3027
  }
3013
- async inspect(forInitialization = false) {
3014
- const snapshot = await safeRootsSnapshot(this.rootsProvider, this.rootsTimeoutMs);
3028
+ async inspect(forInitialization = false, call) {
3029
+ const snapshot = await safeRootsSnapshot(this.rootsProvider, this.rootsTimeoutMs, call);
3015
3030
  const rootCandidates = snapshot.roots.map(inspectRoot);
3016
3031
  const initializedRoots = rootCandidates.filter(
3017
3032
  (candidate) => candidate.initialized && candidate.path !== void 0
@@ -3139,11 +3154,16 @@ function fileRootUriToPath(uri, windows = process.platform === "win32") {
3139
3154
  if (parsed.protocol !== "file:") throw new Error("Root URI is not a file URI");
3140
3155
  return fileURLToPath(parsed, { windows });
3141
3156
  }
3142
- async function safeRootsSnapshot(provider, timeoutMs = MCP_ROOTS_TIMEOUT_MS) {
3157
+ async function safeRootsSnapshot(provider, timeoutMs = MCP_ROOTS_TIMEOUT_MS, call) {
3143
3158
  if (!provider) return { supported: false, roots: [] };
3144
3159
  try {
3145
- return await withOperationTimeout("MCP Roots request", timeoutMs, () => provider());
3160
+ return await withOperationTimeout(
3161
+ "MCP Roots request",
3162
+ timeoutMs,
3163
+ () => provider(call)
3164
+ );
3146
3165
  } catch (error) {
3166
+ if (error instanceof McpRootsPending) throw error;
3147
3167
  return {
3148
3168
  supported: true,
3149
3169
  roots: [],
@@ -3221,7 +3241,7 @@ var TARGET_MCP_TOOL_NAMES = [
3221
3241
  "support",
3222
3242
  "report"
3223
3243
  ];
3224
- var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
3244
+ var STRUCTURED_TOOL_OUTPUT_SCHEMA = z.object({
3225
3245
  schemaVersion: z.literal(1),
3226
3246
  outcome: z.enum([
3227
3247
  "completed",
@@ -3303,7 +3323,7 @@ var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
3303
3323
  reasonCode: z.string().optional()
3304
3324
  })
3305
3325
  )
3306
- };
3326
+ });
3307
3327
  function structuredToolResult(envelope) {
3308
3328
  const clientTimeZone = clientRuntimeTimeZone();
3309
3329
  const presentation = {
@@ -3417,9 +3437,9 @@ function resolverFor(ctx) {
3417
3437
  }
3418
3438
  return resolver;
3419
3439
  }
3420
- async function withProjectDir(ctx) {
3440
+ async function withProjectDir(ctx, call) {
3421
3441
  try {
3422
- const binding = await resolverFor(ctx).resolve();
3442
+ const binding = await resolverFor(ctx).resolve(call);
3423
3443
  const resolved = resolveLockedProjectRoot(binding.projectDir);
3424
3444
  return {
3425
3445
  ...ctx,
@@ -3442,12 +3462,12 @@ async function withProjectDir(ctx) {
3442
3462
  throw error;
3443
3463
  }
3444
3464
  }
3445
- async function diagnoseProjectBinding(ctx) {
3446
- return resolverFor(ctx).diagnose();
3465
+ async function diagnoseProjectBinding(ctx, call) {
3466
+ return resolverFor(ctx).diagnose(call);
3447
3467
  }
3448
- async function initializeWorkspaceProject(ctx) {
3468
+ async function initializeWorkspaceProject(ctx, call) {
3449
3469
  try {
3450
- const resolved = await resolverFor(ctx).initialize();
3470
+ const resolved = await resolverFor(ctx).initialize(call);
3451
3471
  return {
3452
3472
  ...ctx,
3453
3473
  projectDir: resolved.projectDir,
@@ -3463,10 +3483,11 @@ async function initializeWorkspaceProject(ctx) {
3463
3483
  throw error;
3464
3484
  }
3465
3485
  }
3466
- async function optionalProjectContext(ctx) {
3486
+ async function optionalProjectContext(ctx, call) {
3467
3487
  try {
3468
- return await withProjectDir(ctx);
3469
- } catch {
3488
+ return await withProjectDir(ctx, call);
3489
+ } catch (error) {
3490
+ if (error instanceof McpRootsPending) throw error;
3470
3491
  return null;
3471
3492
  }
3472
3493
  }
@@ -3514,6 +3535,9 @@ function requireSiteFile(ctx) {
3514
3535
  }
3515
3536
  var UNAUTHORIZED_SUMMARY = "The server rejected the site credential: the one in .sakupa/site.json no longer matches the server-side verifier. The site itself is intact on the server \u2014 only the local binding file is the problem. Repair the file (restore a backup or undo the local edit). Do NOT delete the .sakupa directory to work around this: the credential is unrecoverable by design, so abandoning it permanently orphans the existing site.";
3516
3537
  function toolError(e) {
3538
+ if (e instanceof McpRootsPending) {
3539
+ return inputRequired({ inputRequests: { roots: inputRequired.listRoots() } });
3540
+ }
3517
3541
  const isSakupa = isSakupaError(e);
3518
3542
  const errorCode = isSakupa ? e.code : "internal";
3519
3543
  const rawDetails = isSakupaError(e) && e.details && typeof e.details === "object" ? e.details : void 0;
@@ -4030,13 +4054,13 @@ function registerTools(server, baseCtx) {
4030
4054
  description: "Analyze the local project and decide whether it can be deployed as a static site. Detects the framework, the built static output directory (dist/build/out/...), missing index.html, SSR/API-route/database-runtime risks, SPA fallback needs, forbidden files (secrets, .env, archives, media) and size limits. Sakupa deploys ONLY prebuilt static output \u2014 never source, secrets or server code. Run this before deploy.",
4031
4055
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4032
4056
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
4033
- inputSchema: {
4057
+ inputSchema: z2.object({
4034
4058
  outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection).")
4035
- }
4059
+ })
4036
4060
  },
4037
- async (args) => {
4061
+ async (args, call) => {
4038
4062
  try {
4039
- const ctx = await withProjectDir(baseCtx);
4063
+ const ctx = await withProjectDir(baseCtx, call);
4040
4064
  const analysis = await analyzeProject(ctx.projectDir, {
4041
4065
  ...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
4042
4066
  });
@@ -4057,7 +4081,7 @@ Next action: ${analysis.suggestedNextAction}`,
4057
4081
  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.`,
4058
4082
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4059
4083
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
4060
- inputSchema: {
4084
+ inputSchema: z2.object({
4061
4085
  outputDir: z2.string().min(1).describe(
4062
4086
  'REQUIRED: exact publish directory relative to the initialized project root, supplied by the AI after inspecting this project (for example ".", "dist", "html", or any custom build directory). Sakupa applies it only inside the cwd-locked project.'
4063
4087
  ),
@@ -4083,12 +4107,12 @@ Next action: ${analysis.suggestedNextAction}`,
4083
4107
  "Deprecated compatibility field. Project independence is established only by `sakupa-mcp init`, never inferred from package.json or folder names."
4084
4108
  ),
4085
4109
  lang: z2.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
4086
- }
4110
+ })
4087
4111
  },
4088
- async (args) => {
4112
+ async (args, call) => {
4089
4113
  let releaseHandoffLock;
4090
4114
  try {
4091
- const ctx = await withProjectDir(baseCtx);
4115
+ const ctx = await withProjectDir(baseCtx, call);
4092
4116
  const analysis = await analyzeProject(ctx.projectDir, { outputDir: args.outputDir });
4093
4117
  if (!analysis.deployable || !analysis.files) {
4094
4118
  return notDeployableResult(analysis);
@@ -4713,11 +4737,11 @@ Optional security recommendation: this management credential was created at ${ti
4713
4737
  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.",
4714
4738
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4715
4739
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
4716
- inputSchema: {}
4740
+ inputSchema: z2.object({})
4717
4741
  },
4718
- async () => {
4742
+ async (_args, call) => {
4719
4743
  try {
4720
- const ctx = await withProjectDir(baseCtx);
4744
+ const ctx = await withProjectDir(baseCtx, call);
4721
4745
  const site = requireSiteFile(ctx);
4722
4746
  const res = await ctx.client.refreshSite(site.siteId, site.credential);
4723
4747
  if (site.url) {
@@ -4746,11 +4770,11 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
4746
4770
  description: "Show the current status of this project's Sakupa site: URL, mode (free/paid), expiry, custom domains, size, last deployment and warnings. For a paid site this tool also automatically returns the complete authoritative billing snapshot; users never need to know or name a separate billing tool to get accurate subscription information.",
4747
4771
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4748
4772
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
4749
- inputSchema: {}
4773
+ inputSchema: z2.object({})
4750
4774
  },
4751
- async () => {
4775
+ async (_args, call) => {
4752
4776
  try {
4753
- const ctx = await withProjectDir(baseCtx);
4777
+ const ctx = await withProjectDir(baseCtx, call);
4754
4778
  const site = requireSiteFile(ctx);
4755
4779
  const res = await ctx.client.getSiteStatus(site.siteId, site.credential);
4756
4780
  noteSiteMode(res.siteId, res.mode);
@@ -4777,15 +4801,15 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
4777
4801
  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.`,
4778
4802
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4779
4803
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
4780
- inputSchema: {
4804
+ inputSchema: z2.object({
4781
4805
  plan: planEnum.describe(
4782
4806
  "Monthly plan: water (very light personal pages), personal (personal brand / small shop), share (small-business site), business (steadier traffic, more headroom)."
4783
4807
  )
4784
- }
4808
+ })
4785
4809
  },
4786
- async (args) => {
4810
+ async (args, call) => {
4787
4811
  try {
4788
- const ctx = await withProjectDir(baseCtx);
4812
+ const ctx = await withProjectDir(baseCtx, call);
4789
4813
  const site = requireSiteFile(ctx);
4790
4814
  const res = await ctx.client.createPlanCheckout(
4791
4815
  {
@@ -4823,17 +4847,17 @@ Once Stripe confirms payment and Sakupa synchronizes the subscription, the curre
4823
4847
  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. A site has one FINAL apex domain; starting a different apex begins a zero-downtime switch and the previous domain remains until the new www is live. The www CNAME must remain while bound. 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.`,
4824
4848
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4825
4849
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
4826
- inputSchema: {
4850
+ inputSchema: z2.object({
4827
4851
  action: z2.enum(["start", "status"]),
4828
4852
  hostname: z2.string().optional().describe("Required for start."),
4829
4853
  verificationId: z2.string().optional().describe(
4830
4854
  "Optional for status: when omitted, the server finds this site's latest binding verification \u2014 a NEW session can resume without it."
4831
4855
  )
4832
- }
4856
+ })
4833
4857
  },
4834
- async (args) => {
4858
+ async (args, call) => {
4835
4859
  try {
4836
- const ctx = await withProjectDir(baseCtx);
4860
+ const ctx = await withProjectDir(baseCtx, call);
4837
4861
  const site = requireSiteFile(ctx);
4838
4862
  if (args.action === "status") {
4839
4863
  const res2 = args.verificationId ? await ctx.client.checkVerification(args.verificationId, site.credential) : await ctx.client.checkVerification("latest", site.credential, site.siteId);
@@ -4962,11 +4986,11 @@ When the user says the TXT is set, run bind "status". It verifies ownership and
4962
4986
  description: "Return the sole authoritative source for this site's hosting subscription: current plan, next renewal plan or cancellation, effective time, payment state, current paid entitlement, reconciled paid usage or current free-site fair-use telemetry, estimated usage tier, bound custom domains and risks. Owner-only (uses the credential in .sakupa/site.json).",
4963
4987
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4964
4988
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
4965
- inputSchema: {}
4989
+ inputSchema: z2.object({})
4966
4990
  },
4967
- async () => {
4991
+ async (_args, call) => {
4968
4992
  try {
4969
- const ctx = await withProjectDir(baseCtx);
4993
+ const ctx = await withProjectDir(baseCtx, call);
4970
4994
  const site = requireSiteFile(ctx);
4971
4995
  const res = await ctx.client.getBillingStatus(site.siteId, site.credential);
4972
4996
  noteSiteMode(res.siteId, res.mode);
@@ -5006,14 +5030,14 @@ Full status:`, res);
5006
5030
  description: "Open the Stripe-hosted billing portal for this site: update the payment method, view invoices, or cancel the subscription. All billing operations happen on the Stripe-hosted page \u2014 never inside the AI tool. With .sakupa/site.json, this opens the site-specific portal. Without the local credential, this returns Stripe's public no-code Customer Portal login page. The customer enters the checkout email and confirms a one-time passcode sent by Stripe. This never restores Sakupa site authority.",
5007
5031
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5008
5032
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
5009
- inputSchema: {
5033
+ inputSchema: z2.object({
5010
5034
  scope: z2.enum(["site", "public_recovery"])
5011
- }
5035
+ })
5012
5036
  },
5013
- async (args) => {
5037
+ async (args, call) => {
5014
5038
  try {
5015
5039
  if (args.scope === "site") {
5016
- const ctx = await withProjectDir(baseCtx);
5040
+ const ctx = await withProjectDir(baseCtx, call);
5017
5041
  const site = requireSiteFile(ctx);
5018
5042
  const res2 = await ctx.client.createBillingPortal(site.siteId, site.credential);
5019
5043
  return structuredToolResult({
@@ -5062,7 +5086,7 @@ Full status:`, res);
5062
5086
  description: "Recover management control of a subscribed site WITH A BOUND CUSTOM DOMAIN after losing the local project, by proving DNS control of the apex domain. Sites without a bound domain are identified solely by their local credential and cannot be recovered. By default, completing recovery REVOKES all previous local credentials. Recovery is resumable: start stores local pending state; complete installs and writes the new .sakupa/site.json credential BEFORE requesting content; download uses that credential to reissue an archive and safely extract it into the explicitly selected outputDir without repeating DNS.",
5063
5087
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5064
5088
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
5065
- inputSchema: {
5089
+ inputSchema: z2.object({
5066
5090
  action: z2.enum(["start", "status", "complete", "download"]),
5067
5091
  hostname: z2.string().optional().describe("Required for start."),
5068
5092
  verificationId: z2.string().optional().describe("For status or complete; inferred from local recovery state when omitted."),
@@ -5070,11 +5094,11 @@ Full status:`, res);
5070
5094
  "REQUIRED for complete/download: exact extraction directory relative to the initialized project root. Inspect the current project; Sakupa never guesses a name."
5071
5095
  ),
5072
5096
  preserveExistingCredentials: z2.boolean().optional().describe("Explicitly keep old local credentials working (default: revoke them all).")
5073
- }
5097
+ })
5074
5098
  },
5075
- async (args) => {
5099
+ async (args, call) => {
5076
5100
  try {
5077
- const ctx = await withProjectDir(baseCtx);
5101
+ const ctx = await withProjectDir(baseCtx, call);
5078
5102
  if ((args.action === "complete" || args.action === "download") && args.outputDir === void 0) {
5079
5103
  throw new LocalGuidanceError(
5080
5104
  "invalid_request",
@@ -5423,16 +5447,16 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
5423
5447
  description: "Create a Sakupa support ticket for billing, payment, refund review, domain verification, deployment, serving or other issues the MCP cannot solve automatically. Do not include secrets, credentials or card data in the description.",
5424
5448
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5425
5449
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
5426
- inputSchema: {
5450
+ inputSchema: z2.object({
5427
5451
  category: ticketCategoryEnum,
5428
5452
  subject: z2.string().describe("Short subject line."),
5429
5453
  description: z2.string().describe("Problem description (no secrets, no card data)."),
5430
5454
  contactEmail: z2.string().optional().describe("Optional contact email for follow-up.")
5431
- }
5455
+ })
5432
5456
  },
5433
- async (args) => {
5457
+ async (args, call) => {
5434
5458
  try {
5435
- const ctx = await withProjectDir(baseCtx);
5459
+ const ctx = await withProjectDir(baseCtx, call);
5436
5460
  const site = requireSiteFile(ctx);
5437
5461
  const res = await ctx.client.createTicket(site.credential, {
5438
5462
  siteId: site.siteId,
@@ -5457,7 +5481,7 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
5457
5481
  description: "LAST RESORT after help explicitly returns reportRecommended:true. Prepare and submit a sanitized product bug report using helpAuthorization from that diagnosis. Only whitelisted structured diagnostics are sent (tool name, error code/message, site id, bound domain, deployment id, timestamps, client/MCP version, request id) \u2014 NEVER file contents, source code, secrets, .env values or credentials. Without confirmSubmit: true the exact payload is shown for user review and nothing is submitted.",
5458
5482
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5459
5483
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
5460
- inputSchema: {
5484
+ inputSchema: z2.object({
5461
5485
  toolName: z2.string().describe('The Sakupa tool that failed, e.g. "deploy".'),
5462
5486
  helpAuthorization: z2.string().describe("Short-lived authorization returned only by help when report is recommended."),
5463
5487
  errorCode: z2.string().optional(),
@@ -5473,12 +5497,12 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
5473
5497
  "OPTIONAL. Before submitting, ask the user ONCE whether they want to leave a contact for follow-up. Omit entirely if they decline \u2014 never require it."
5474
5498
  ),
5475
5499
  confirmSubmit: z2.boolean().optional().describe("User reviewed the report payload and approved submission.")
5476
- }
5500
+ })
5477
5501
  },
5478
- async (args) => {
5502
+ async (args, call) => {
5479
5503
  try {
5480
5504
  requireReportAuthorization(baseCtx, args.helpAuthorization, args.toolName);
5481
- const ctx = await optionalProjectContext(baseCtx);
5505
+ const ctx = await optionalProjectContext(baseCtx, call);
5482
5506
  const siteState = ctx ? loadSiteFile(ctx.projectDir) : { kind: "absent" };
5483
5507
  const site = siteState.kind === "ok" ? siteState.file : null;
5484
5508
  const diagnostics = {
@@ -5558,11 +5582,11 @@ function registerBillingTools(server, baseCtx) {
5558
5582
  "plans",
5559
5583
  {
5560
5584
  description: "Return the authoritative Sakupa monthly plan catalog, exact limits, prices, catalog version and plan-change billing rules. This is read-only and does not require a site.",
5561
- inputSchema: {},
5585
+ inputSchema: z3.object({}),
5562
5586
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5563
5587
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true }
5564
5588
  },
5565
- async () => {
5589
+ async (_args, call) => {
5566
5590
  try {
5567
5591
  const catalog = await baseCtx.client.getBillingPlanCatalog();
5568
5592
  return structuredToolResult({
@@ -5582,15 +5606,15 @@ function registerBillingTools(server, baseCtx) {
5582
5606
  "change",
5583
5607
  {
5584
5608
  description: "Create one Stripe-hosted subscription-management link. The user chooses the plan or period-end cancellation on Stripe; Sakupa never infers intent from the conversation. Creating the link does not change billing.",
5585
- inputSchema: {
5609
+ inputSchema: z3.object({
5586
5610
  operationId: z3.string().min(1)
5587
- },
5611
+ }),
5588
5612
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5589
5613
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
5590
5614
  },
5591
- async (args) => {
5615
+ async (args, call) => {
5592
5616
  try {
5593
- const ctx = await withProjectDir(baseCtx);
5617
+ const ctx = await withProjectDir(baseCtx, call);
5594
5618
  const site = requireSiteFile(ctx);
5595
5619
  const result = await ctx.client.changeSubscriptionPlan(site.credential, {
5596
5620
  siteId: site.siteId,
@@ -5873,13 +5897,13 @@ function registerHelpTools(server, baseCtx) {
5873
5897
  "init",
5874
5898
  {
5875
5899
  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.",
5876
- inputSchema: {},
5900
+ inputSchema: z4.object({}),
5877
5901
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5878
5902
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
5879
5903
  },
5880
- async () => {
5904
+ async (_args, call) => {
5881
5905
  try {
5882
- const ctx = await initializeWorkspaceProject(baseCtx);
5906
+ const ctx = await initializeWorkspaceProject(baseCtx, call);
5883
5907
  const marker = loadProjectMarker(ctx.projectDir);
5884
5908
  if (marker.kind !== "ok")
5885
5909
  throw new Error("init postcondition failed: project marker missing");
@@ -5913,17 +5937,17 @@ function registerHelpTools(server, baseCtx) {
5913
5937
  "help",
5914
5938
  {
5915
5939
  description: "FIRST troubleshooting tool for every Sakupa difficulty. With topic diagnose (default), inspect MCP Roots, cwd, binding and local state without requiring a project or calling the API. Use overview, terminology, or a tool name for complete usage, side effects, parameters and warnings. Only recommend report when help explicitly returns reportRecommended:true.",
5916
- inputSchema: {
5940
+ inputSchema: z4.object({
5917
5941
  topic: z4.enum(HELP_TOPICS).optional().default("diagnose"),
5918
5942
  failedTool: z4.string().optional(),
5919
5943
  errorCode: z4.string().optional(),
5920
5944
  resultCode: z4.string().optional(),
5921
5945
  requestId: z4.string().optional()
5922
- },
5946
+ }),
5923
5947
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5924
5948
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
5925
5949
  },
5926
- async (args) => {
5950
+ async (args, call) => {
5927
5951
  try {
5928
5952
  if (args.topic === "overview") {
5929
5953
  const catalog = Object.fromEntries(
@@ -5985,7 +6009,7 @@ Terminology: ${terminologyText}` : ""),
5985
6009
  nextActions: []
5986
6010
  });
5987
6011
  }
5988
- const diagnosis = await diagnoseProjectBinding(baseCtx);
6012
+ const diagnosis = await diagnoseProjectBinding(baseCtx, call);
5989
6013
  const selected = diagnosis.selectedProjectDir;
5990
6014
  const marker = selected ? loadProjectMarker(selected) : { kind: "absent" };
5991
6015
  const site = selected ? loadSiteFile(selected) : { kind: "absent" };
@@ -6064,18 +6088,18 @@ function registerCredentialTools(server, baseCtx) {
6064
6088
  "rotate",
6065
6089
  {
6066
6090
  description: "Optionally rotate this site management credential. The first call is a read-only preview. Only confirmed:true after explicit user approval installs a locally generated new credential and revokes every previous credential. Rotation is never required to deploy.",
6067
- inputSchema: {
6091
+ inputSchema: z5.object({
6068
6092
  confirmed: z5.boolean().optional().describe(
6069
6093
  "True only after showing the rotate preview and the user explicitly approves revoking every old credential."
6070
6094
  )
6071
- },
6095
+ }),
6072
6096
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
6073
6097
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
6074
6098
  },
6075
- async (args) => {
6099
+ async (args, call) => {
6076
6100
  let releaseLock;
6077
6101
  try {
6078
- const ctx = await withProjectDir(baseCtx);
6102
+ const ctx = await withProjectDir(baseCtx, call);
6079
6103
  let site = requireSiteFile(ctx);
6080
6104
  const pending = loadCredentialRotation(ctx.projectDir);
6081
6105
  if (pending.kind !== "absent" || args.confirmed === true) {
@@ -6466,23 +6490,7 @@ function createSakupaMcpServer(opts) {
6466
6490
  { instructions: instructionsFor(previewHostPatternFor(opts.apiBaseUrl)) }
6467
6491
  );
6468
6492
  const processCwd = resolve6(opts.projectDir ?? process.cwd());
6469
- const rootsProvider = opts.rootsProvider ?? (async () => {
6470
- const capabilities = server.server.getClientCapabilities();
6471
- if (!capabilities?.roots) return { supported: false, roots: [] };
6472
- try {
6473
- const response = await server.server.listRoots(void 0, {
6474
- timeout: MCP_ROOTS_TIMEOUT_MS,
6475
- maxTotalTimeout: MCP_ROOTS_TIMEOUT_MS
6476
- });
6477
- return { supported: true, roots: response.roots };
6478
- } catch (error) {
6479
- return {
6480
- supported: true,
6481
- roots: [],
6482
- error: error instanceof Error ? error.message : String(error)
6483
- };
6484
- }
6485
- });
6493
+ const rootsProvider = opts.rootsProvider ?? ((call) => readClientRoots(server, call));
6486
6494
  const ctx = {
6487
6495
  client,
6488
6496
  apiBaseUrl: opts.apiBaseUrl,
@@ -6496,6 +6504,38 @@ function createSakupaMcpServer(opts) {
6496
6504
  registerHelpTools(server, ctx);
6497
6505
  return server;
6498
6506
  }
6507
+ async function readClientRoots(server, call) {
6508
+ if (call?.mcpReq.envelope !== void 0) {
6509
+ const envelope = call.mcpReq.envelope;
6510
+ const declared = envelope[CLIENT_CAPABILITIES_META_KEY];
6511
+ if (!declared?.roots) return { supported: false, roots: [] };
6512
+ const answered = inputResponse(call.mcpReq.inputResponses, "roots");
6513
+ if (answered.kind === "roots") return { supported: true, roots: answered.roots };
6514
+ if (call.mcpReq.inputResponses !== void 0) {
6515
+ return {
6516
+ supported: true,
6517
+ roots: [],
6518
+ error: "The client retried without answering the embedded roots/list request."
6519
+ };
6520
+ }
6521
+ throw new McpRootsPending();
6522
+ }
6523
+ const capabilities = server.server.getClientCapabilities();
6524
+ if (!capabilities?.roots) return { supported: false, roots: [] };
6525
+ try {
6526
+ const response = await server.server.listRoots(void 0, {
6527
+ timeout: MCP_ROOTS_TIMEOUT_MS,
6528
+ maxTotalTimeout: MCP_ROOTS_TIMEOUT_MS
6529
+ });
6530
+ return { supported: true, roots: response.roots };
6531
+ } catch (error) {
6532
+ return {
6533
+ supported: true,
6534
+ roots: [],
6535
+ error: error instanceof Error ? error.message : String(error)
6536
+ };
6537
+ }
6538
+ }
6499
6539
 
6500
6540
  // src/bin.ts
6501
6541
  async function main() {
@@ -6508,15 +6548,20 @@ async function main() {
6508
6548
  process.exitCode = result.exitCode;
6509
6549
  return;
6510
6550
  }
6551
+ if (argv[0] === "--version" || argv[0] === "-v") {
6552
+ stdout.write(`${MCP_VERSION}
6553
+ `);
6554
+ return;
6555
+ }
6511
6556
  if (argv.length > 0) {
6512
- throw new Error("Usage: sakupa-mcp [init]");
6557
+ throw new Error("Usage: sakupa-mcp [init | --version]");
6513
6558
  }
6514
6559
  const config = loadMcpRuntimeConfig();
6515
- const server = createSakupaMcpServer(config);
6516
- const transport = new StdioServerTransport();
6517
- await server.connect(transport);
6560
+ serveStdio(() => createSakupaMcpServer(config), {
6561
+ onerror: (error) => console.error("[sakupa-mcp] transport error:", error.message)
6562
+ });
6518
6563
  console.error(
6519
- `[sakupa-mcp] v${MCP_VERSION} connected (api: ${config.apiBaseUrl}; process cwd fallback: ${process.cwd()}; MCP Roots preferred)`
6564
+ `[sakupa-mcp] v${MCP_VERSION} serving stdio (api: ${config.apiBaseUrl}; process cwd fallback: ${process.cwd()}; MCP Roots preferred)`
6520
6565
  );
6521
6566
  }
6522
6567
  main().catch((err2) => {
package/dist/index.js CHANGED
@@ -147,7 +147,7 @@ function isFreeSiteAllowanceNetworkReference(value) {
147
147
  }
148
148
 
149
149
  // ../core/dist/domain/version.js
150
- var SAKUPA_MCP_VERSION = "1.0.0";
150
+ var SAKUPA_MCP_VERSION = "1.1.0";
151
151
 
152
152
  // ../core/dist/domain/errors.js
153
153
  var HTTP_STATUS = {
@@ -1536,6 +1536,11 @@ async function analyzeProject(projectDir, opts = {}) {
1536
1536
  };
1537
1537
  }
1538
1538
 
1539
+ // src/tools/context.ts
1540
+ import {
1541
+ inputRequired
1542
+ } from "@modelcontextprotocol/server";
1543
+
1539
1544
  // src/project-binding.ts
1540
1545
  import { fileURLToPath } from "node:url";
1541
1546
  import { resolve as resolve3 } from "node:path";
@@ -1763,6 +1768,12 @@ function writeMarkerAtomically(projectDir, marker) {
1763
1768
 
1764
1769
  // src/project-binding.ts
1765
1770
  var MCP_ROOTS_TIMEOUT_MS = 5e3;
1771
+ var McpRootsPending = class extends Error {
1772
+ constructor() {
1773
+ super("MCP Roots must be requested from the client before the project can be bound.");
1774
+ this.name = "McpRootsPending";
1775
+ }
1776
+ };
1766
1777
  var ProjectBindingError = class extends Error {
1767
1778
  diagnostics;
1768
1779
  constructor(diagnostics) {
@@ -1780,10 +1791,10 @@ var ProjectBindingResolver = class {
1780
1791
  bound;
1781
1792
  boundState;
1782
1793
  resolving;
1783
- async resolve() {
1794
+ async resolve(call) {
1784
1795
  if (this.bound) return this.bound;
1785
1796
  if (this.resolving) return this.resolving;
1786
- this.resolving = this.inspect().then((inspection) => {
1797
+ this.resolving = this.inspect(false, call).then((inspection) => {
1787
1798
  if (!inspection.selected) throw new ProjectBindingError(inspection.diagnostics);
1788
1799
  this.bound = inspection.selected;
1789
1800
  this.boundState = inspection.diagnostics;
@@ -1793,18 +1804,18 @@ var ProjectBindingResolver = class {
1793
1804
  });
1794
1805
  return this.resolving;
1795
1806
  }
1796
- async diagnose() {
1807
+ async diagnose(call) {
1797
1808
  if (this.bound) return this.boundState ?? boundDiagnostics(this.processCwd, this.bound);
1798
- const inspection = await this.inspect();
1809
+ const inspection = await this.inspect(false, call);
1799
1810
  if (inspection.selected) {
1800
1811
  this.bound = inspection.selected;
1801
1812
  this.boundState = inspection.diagnostics;
1802
1813
  }
1803
1814
  return inspection.diagnostics;
1804
1815
  }
1805
- async initialize() {
1816
+ async initialize(call) {
1806
1817
  if (this.bound) return this.bound;
1807
- const inspection = await this.inspect(true);
1818
+ const inspection = await this.inspect(true, call);
1808
1819
  if (inspection.selected) {
1809
1820
  this.bound = inspection.selected;
1810
1821
  this.boundState = inspection.diagnostics;
@@ -1823,8 +1834,8 @@ var ProjectBindingResolver = class {
1823
1834
  );
1824
1835
  return this.bound;
1825
1836
  }
1826
- async inspect(forInitialization = false) {
1827
- const snapshot = await safeRootsSnapshot(this.rootsProvider, this.rootsTimeoutMs);
1837
+ async inspect(forInitialization = false, call) {
1838
+ const snapshot = await safeRootsSnapshot(this.rootsProvider, this.rootsTimeoutMs, call);
1828
1839
  const rootCandidates = snapshot.roots.map(inspectRoot);
1829
1840
  const initializedRoots = rootCandidates.filter(
1830
1841
  (candidate) => candidate.initialized && candidate.path !== void 0
@@ -1952,11 +1963,16 @@ function fileRootUriToPath(uri, windows = process.platform === "win32") {
1952
1963
  if (parsed.protocol !== "file:") throw new Error("Root URI is not a file URI");
1953
1964
  return fileURLToPath(parsed, { windows });
1954
1965
  }
1955
- async function safeRootsSnapshot(provider, timeoutMs = MCP_ROOTS_TIMEOUT_MS) {
1966
+ async function safeRootsSnapshot(provider, timeoutMs = MCP_ROOTS_TIMEOUT_MS, call) {
1956
1967
  if (!provider) return { supported: false, roots: [] };
1957
1968
  try {
1958
- return await withOperationTimeout("MCP Roots request", timeoutMs, () => provider());
1969
+ return await withOperationTimeout(
1970
+ "MCP Roots request",
1971
+ timeoutMs,
1972
+ () => provider(call)
1973
+ );
1959
1974
  } catch (error) {
1975
+ if (error instanceof McpRootsPending) throw error;
1960
1976
  return {
1961
1977
  supported: true,
1962
1978
  roots: [],
@@ -2034,7 +2050,7 @@ var TARGET_MCP_TOOL_NAMES = [
2034
2050
  "support",
2035
2051
  "report"
2036
2052
  ];
2037
- var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
2053
+ var STRUCTURED_TOOL_OUTPUT_SCHEMA = z.object({
2038
2054
  schemaVersion: z.literal(1),
2039
2055
  outcome: z.enum([
2040
2056
  "completed",
@@ -2116,7 +2132,7 @@ var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
2116
2132
  reasonCode: z.string().optional()
2117
2133
  })
2118
2134
  )
2119
- };
2135
+ });
2120
2136
  function structuredToolResult(envelope) {
2121
2137
  const clientTimeZone = clientRuntimeTimeZone();
2122
2138
  const presentation = {
@@ -2230,9 +2246,9 @@ function resolverFor(ctx) {
2230
2246
  }
2231
2247
  return resolver;
2232
2248
  }
2233
- async function withProjectDir(ctx) {
2249
+ async function withProjectDir(ctx, call) {
2234
2250
  try {
2235
- const binding = await resolverFor(ctx).resolve();
2251
+ const binding = await resolverFor(ctx).resolve(call);
2236
2252
  const resolved = resolveLockedProjectRoot(binding.projectDir);
2237
2253
  return {
2238
2254
  ...ctx,
@@ -2255,12 +2271,12 @@ async function withProjectDir(ctx) {
2255
2271
  throw error;
2256
2272
  }
2257
2273
  }
2258
- async function diagnoseProjectBinding(ctx) {
2259
- return resolverFor(ctx).diagnose();
2274
+ async function diagnoseProjectBinding(ctx, call) {
2275
+ return resolverFor(ctx).diagnose(call);
2260
2276
  }
2261
- async function initializeWorkspaceProject(ctx) {
2277
+ async function initializeWorkspaceProject(ctx, call) {
2262
2278
  try {
2263
- const resolved = await resolverFor(ctx).initialize();
2279
+ const resolved = await resolverFor(ctx).initialize(call);
2264
2280
  return {
2265
2281
  ...ctx,
2266
2282
  projectDir: resolved.projectDir,
@@ -2276,10 +2292,11 @@ async function initializeWorkspaceProject(ctx) {
2276
2292
  throw error;
2277
2293
  }
2278
2294
  }
2279
- async function optionalProjectContext(ctx) {
2295
+ async function optionalProjectContext(ctx, call) {
2280
2296
  try {
2281
- return await withProjectDir(ctx);
2282
- } catch {
2297
+ return await withProjectDir(ctx, call);
2298
+ } catch (error) {
2299
+ if (error instanceof McpRootsPending) throw error;
2283
2300
  return null;
2284
2301
  }
2285
2302
  }
@@ -2327,6 +2344,9 @@ function requireSiteFile(ctx) {
2327
2344
  }
2328
2345
  var UNAUTHORIZED_SUMMARY = "The server rejected the site credential: the one in .sakupa/site.json no longer matches the server-side verifier. The site itself is intact on the server \u2014 only the local binding file is the problem. Repair the file (restore a backup or undo the local edit). Do NOT delete the .sakupa directory to work around this: the credential is unrecoverable by design, so abandoning it permanently orphans the existing site.";
2329
2346
  function toolError(e) {
2347
+ if (e instanceof McpRootsPending) {
2348
+ return inputRequired({ inputRequests: { roots: inputRequired.listRoots() } });
2349
+ }
2330
2350
  const isSakupa = isSakupaError(e);
2331
2351
  const errorCode = isSakupa ? e.code : "internal";
2332
2352
  const rawDetails = isSakupaError(e) && e.details && typeof e.details === "object" ? e.details : void 0;
@@ -4146,13 +4166,13 @@ function registerTools(server, baseCtx) {
4146
4166
  description: "Analyze the local project and decide whether it can be deployed as a static site. Detects the framework, the built static output directory (dist/build/out/...), missing index.html, SSR/API-route/database-runtime risks, SPA fallback needs, forbidden files (secrets, .env, archives, media) and size limits. Sakupa deploys ONLY prebuilt static output \u2014 never source, secrets or server code. Run this before deploy.",
4147
4167
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4148
4168
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
4149
- inputSchema: {
4169
+ inputSchema: z2.object({
4150
4170
  outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection).")
4151
- }
4171
+ })
4152
4172
  },
4153
- async (args) => {
4173
+ async (args, call) => {
4154
4174
  try {
4155
- const ctx = await withProjectDir(baseCtx);
4175
+ const ctx = await withProjectDir(baseCtx, call);
4156
4176
  const analysis = await analyzeProject(ctx.projectDir, {
4157
4177
  ...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
4158
4178
  });
@@ -4173,7 +4193,7 @@ Next action: ${analysis.suggestedNextAction}`,
4173
4193
  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.`,
4174
4194
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4175
4195
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
4176
- inputSchema: {
4196
+ inputSchema: z2.object({
4177
4197
  outputDir: z2.string().min(1).describe(
4178
4198
  'REQUIRED: exact publish directory relative to the initialized project root, supplied by the AI after inspecting this project (for example ".", "dist", "html", or any custom build directory). Sakupa applies it only inside the cwd-locked project.'
4179
4199
  ),
@@ -4199,12 +4219,12 @@ Next action: ${analysis.suggestedNextAction}`,
4199
4219
  "Deprecated compatibility field. Project independence is established only by `sakupa-mcp init`, never inferred from package.json or folder names."
4200
4220
  ),
4201
4221
  lang: z2.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
4202
- }
4222
+ })
4203
4223
  },
4204
- async (args) => {
4224
+ async (args, call) => {
4205
4225
  let releaseHandoffLock;
4206
4226
  try {
4207
- const ctx = await withProjectDir(baseCtx);
4227
+ const ctx = await withProjectDir(baseCtx, call);
4208
4228
  const analysis = await analyzeProject(ctx.projectDir, { outputDir: args.outputDir });
4209
4229
  if (!analysis.deployable || !analysis.files) {
4210
4230
  return notDeployableResult(analysis);
@@ -4829,11 +4849,11 @@ Optional security recommendation: this management credential was created at ${ti
4829
4849
  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.",
4830
4850
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4831
4851
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
4832
- inputSchema: {}
4852
+ inputSchema: z2.object({})
4833
4853
  },
4834
- async () => {
4854
+ async (_args, call) => {
4835
4855
  try {
4836
- const ctx = await withProjectDir(baseCtx);
4856
+ const ctx = await withProjectDir(baseCtx, call);
4837
4857
  const site = requireSiteFile(ctx);
4838
4858
  const res = await ctx.client.refreshSite(site.siteId, site.credential);
4839
4859
  if (site.url) {
@@ -4862,11 +4882,11 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
4862
4882
  description: "Show the current status of this project's Sakupa site: URL, mode (free/paid), expiry, custom domains, size, last deployment and warnings. For a paid site this tool also automatically returns the complete authoritative billing snapshot; users never need to know or name a separate billing tool to get accurate subscription information.",
4863
4883
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4864
4884
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
4865
- inputSchema: {}
4885
+ inputSchema: z2.object({})
4866
4886
  },
4867
- async () => {
4887
+ async (_args, call) => {
4868
4888
  try {
4869
- const ctx = await withProjectDir(baseCtx);
4889
+ const ctx = await withProjectDir(baseCtx, call);
4870
4890
  const site = requireSiteFile(ctx);
4871
4891
  const res = await ctx.client.getSiteStatus(site.siteId, site.credential);
4872
4892
  noteSiteMode(res.siteId, res.mode);
@@ -4893,15 +4913,15 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
4893
4913
  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.`,
4894
4914
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4895
4915
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
4896
- inputSchema: {
4916
+ inputSchema: z2.object({
4897
4917
  plan: planEnum.describe(
4898
4918
  "Monthly plan: water (very light personal pages), personal (personal brand / small shop), share (small-business site), business (steadier traffic, more headroom)."
4899
4919
  )
4900
- }
4920
+ })
4901
4921
  },
4902
- async (args) => {
4922
+ async (args, call) => {
4903
4923
  try {
4904
- const ctx = await withProjectDir(baseCtx);
4924
+ const ctx = await withProjectDir(baseCtx, call);
4905
4925
  const site = requireSiteFile(ctx);
4906
4926
  const res = await ctx.client.createPlanCheckout(
4907
4927
  {
@@ -4939,17 +4959,17 @@ Once Stripe confirms payment and Sakupa synchronizes the subscription, the curre
4939
4959
  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. A site has one FINAL apex domain; starting a different apex begins a zero-downtime switch and the previous domain remains until the new www is live. The www CNAME must remain while bound. 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.`,
4940
4960
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
4941
4961
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
4942
- inputSchema: {
4962
+ inputSchema: z2.object({
4943
4963
  action: z2.enum(["start", "status"]),
4944
4964
  hostname: z2.string().optional().describe("Required for start."),
4945
4965
  verificationId: z2.string().optional().describe(
4946
4966
  "Optional for status: when omitted, the server finds this site's latest binding verification \u2014 a NEW session can resume without it."
4947
4967
  )
4948
- }
4968
+ })
4949
4969
  },
4950
- async (args) => {
4970
+ async (args, call) => {
4951
4971
  try {
4952
- const ctx = await withProjectDir(baseCtx);
4972
+ const ctx = await withProjectDir(baseCtx, call);
4953
4973
  const site = requireSiteFile(ctx);
4954
4974
  if (args.action === "status") {
4955
4975
  const res2 = args.verificationId ? await ctx.client.checkVerification(args.verificationId, site.credential) : await ctx.client.checkVerification("latest", site.credential, site.siteId);
@@ -5078,11 +5098,11 @@ When the user says the TXT is set, run bind "status". It verifies ownership and
5078
5098
  description: "Return the sole authoritative source for this site's hosting subscription: current plan, next renewal plan or cancellation, effective time, payment state, current paid entitlement, reconciled paid usage or current free-site fair-use telemetry, estimated usage tier, bound custom domains and risks. Owner-only (uses the credential in .sakupa/site.json).",
5079
5099
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5080
5100
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
5081
- inputSchema: {}
5101
+ inputSchema: z2.object({})
5082
5102
  },
5083
- async () => {
5103
+ async (_args, call) => {
5084
5104
  try {
5085
- const ctx = await withProjectDir(baseCtx);
5105
+ const ctx = await withProjectDir(baseCtx, call);
5086
5106
  const site = requireSiteFile(ctx);
5087
5107
  const res = await ctx.client.getBillingStatus(site.siteId, site.credential);
5088
5108
  noteSiteMode(res.siteId, res.mode);
@@ -5122,14 +5142,14 @@ Full status:`, res);
5122
5142
  description: "Open the Stripe-hosted billing portal for this site: update the payment method, view invoices, or cancel the subscription. All billing operations happen on the Stripe-hosted page \u2014 never inside the AI tool. With .sakupa/site.json, this opens the site-specific portal. Without the local credential, this returns Stripe's public no-code Customer Portal login page. The customer enters the checkout email and confirms a one-time passcode sent by Stripe. This never restores Sakupa site authority.",
5123
5143
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5124
5144
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
5125
- inputSchema: {
5145
+ inputSchema: z2.object({
5126
5146
  scope: z2.enum(["site", "public_recovery"])
5127
- }
5147
+ })
5128
5148
  },
5129
- async (args) => {
5149
+ async (args, call) => {
5130
5150
  try {
5131
5151
  if (args.scope === "site") {
5132
- const ctx = await withProjectDir(baseCtx);
5152
+ const ctx = await withProjectDir(baseCtx, call);
5133
5153
  const site = requireSiteFile(ctx);
5134
5154
  const res2 = await ctx.client.createBillingPortal(site.siteId, site.credential);
5135
5155
  return structuredToolResult({
@@ -5178,7 +5198,7 @@ Full status:`, res);
5178
5198
  description: "Recover management control of a subscribed site WITH A BOUND CUSTOM DOMAIN after losing the local project, by proving DNS control of the apex domain. Sites without a bound domain are identified solely by their local credential and cannot be recovered. By default, completing recovery REVOKES all previous local credentials. Recovery is resumable: start stores local pending state; complete installs and writes the new .sakupa/site.json credential BEFORE requesting content; download uses that credential to reissue an archive and safely extract it into the explicitly selected outputDir without repeating DNS.",
5179
5199
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5180
5200
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
5181
- inputSchema: {
5201
+ inputSchema: z2.object({
5182
5202
  action: z2.enum(["start", "status", "complete", "download"]),
5183
5203
  hostname: z2.string().optional().describe("Required for start."),
5184
5204
  verificationId: z2.string().optional().describe("For status or complete; inferred from local recovery state when omitted."),
@@ -5186,11 +5206,11 @@ Full status:`, res);
5186
5206
  "REQUIRED for complete/download: exact extraction directory relative to the initialized project root. Inspect the current project; Sakupa never guesses a name."
5187
5207
  ),
5188
5208
  preserveExistingCredentials: z2.boolean().optional().describe("Explicitly keep old local credentials working (default: revoke them all).")
5189
- }
5209
+ })
5190
5210
  },
5191
- async (args) => {
5211
+ async (args, call) => {
5192
5212
  try {
5193
- const ctx = await withProjectDir(baseCtx);
5213
+ const ctx = await withProjectDir(baseCtx, call);
5194
5214
  if ((args.action === "complete" || args.action === "download") && args.outputDir === void 0) {
5195
5215
  throw new LocalGuidanceError(
5196
5216
  "invalid_request",
@@ -5539,16 +5559,16 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
5539
5559
  description: "Create a Sakupa support ticket for billing, payment, refund review, domain verification, deployment, serving or other issues the MCP cannot solve automatically. Do not include secrets, credentials or card data in the description.",
5540
5560
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5541
5561
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
5542
- inputSchema: {
5562
+ inputSchema: z2.object({
5543
5563
  category: ticketCategoryEnum,
5544
5564
  subject: z2.string().describe("Short subject line."),
5545
5565
  description: z2.string().describe("Problem description (no secrets, no card data)."),
5546
5566
  contactEmail: z2.string().optional().describe("Optional contact email for follow-up.")
5547
- }
5567
+ })
5548
5568
  },
5549
- async (args) => {
5569
+ async (args, call) => {
5550
5570
  try {
5551
- const ctx = await withProjectDir(baseCtx);
5571
+ const ctx = await withProjectDir(baseCtx, call);
5552
5572
  const site = requireSiteFile(ctx);
5553
5573
  const res = await ctx.client.createTicket(site.credential, {
5554
5574
  siteId: site.siteId,
@@ -5573,7 +5593,7 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
5573
5593
  description: "LAST RESORT after help explicitly returns reportRecommended:true. Prepare and submit a sanitized product bug report using helpAuthorization from that diagnosis. Only whitelisted structured diagnostics are sent (tool name, error code/message, site id, bound domain, deployment id, timestamps, client/MCP version, request id) \u2014 NEVER file contents, source code, secrets, .env values or credentials. Without confirmSubmit: true the exact payload is shown for user review and nothing is submitted.",
5574
5594
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5575
5595
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
5576
- inputSchema: {
5596
+ inputSchema: z2.object({
5577
5597
  toolName: z2.string().describe('The Sakupa tool that failed, e.g. "deploy".'),
5578
5598
  helpAuthorization: z2.string().describe("Short-lived authorization returned only by help when report is recommended."),
5579
5599
  errorCode: z2.string().optional(),
@@ -5589,12 +5609,12 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
5589
5609
  "OPTIONAL. Before submitting, ask the user ONCE whether they want to leave a contact for follow-up. Omit entirely if they decline \u2014 never require it."
5590
5610
  ),
5591
5611
  confirmSubmit: z2.boolean().optional().describe("User reviewed the report payload and approved submission.")
5592
- }
5612
+ })
5593
5613
  },
5594
- async (args) => {
5614
+ async (args, call) => {
5595
5615
  try {
5596
5616
  requireReportAuthorization(baseCtx, args.helpAuthorization, args.toolName);
5597
- const ctx = await optionalProjectContext(baseCtx);
5617
+ const ctx = await optionalProjectContext(baseCtx, call);
5598
5618
  const siteState = ctx ? loadSiteFile(ctx.projectDir) : { kind: "absent" };
5599
5619
  const site = siteState.kind === "ok" ? siteState.file : null;
5600
5620
  const diagnostics = {
@@ -5668,7 +5688,11 @@ Summary: ${res.sanitizedSummary}`,
5668
5688
  }
5669
5689
 
5670
5690
  // src/server.ts
5671
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5691
+ import {
5692
+ CLIENT_CAPABILITIES_META_KEY,
5693
+ McpServer,
5694
+ inputResponse
5695
+ } from "@modelcontextprotocol/server";
5672
5696
 
5673
5697
  // src/tools/billing.ts
5674
5698
  import { z as z3 } from "zod";
@@ -5677,11 +5701,11 @@ function registerBillingTools(server, baseCtx) {
5677
5701
  "plans",
5678
5702
  {
5679
5703
  description: "Return the authoritative Sakupa monthly plan catalog, exact limits, prices, catalog version and plan-change billing rules. This is read-only and does not require a site.",
5680
- inputSchema: {},
5704
+ inputSchema: z3.object({}),
5681
5705
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5682
5706
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true }
5683
5707
  },
5684
- async () => {
5708
+ async (_args, call) => {
5685
5709
  try {
5686
5710
  const catalog = await baseCtx.client.getBillingPlanCatalog();
5687
5711
  return structuredToolResult({
@@ -5701,15 +5725,15 @@ function registerBillingTools(server, baseCtx) {
5701
5725
  "change",
5702
5726
  {
5703
5727
  description: "Create one Stripe-hosted subscription-management link. The user chooses the plan or period-end cancellation on Stripe; Sakupa never infers intent from the conversation. Creating the link does not change billing.",
5704
- inputSchema: {
5728
+ inputSchema: z3.object({
5705
5729
  operationId: z3.string().min(1)
5706
- },
5730
+ }),
5707
5731
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5708
5732
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
5709
5733
  },
5710
- async (args) => {
5734
+ async (args, call) => {
5711
5735
  try {
5712
- const ctx = await withProjectDir(baseCtx);
5736
+ const ctx = await withProjectDir(baseCtx, call);
5713
5737
  const site = requireSiteFile(ctx);
5714
5738
  const result = await ctx.client.changeSubscriptionPlan(site.credential, {
5715
5739
  siteId: site.siteId,
@@ -5992,13 +6016,13 @@ function registerHelpTools(server, baseCtx) {
5992
6016
  "init",
5993
6017
  {
5994
6018
  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.",
5995
- inputSchema: {},
6019
+ inputSchema: z4.object({}),
5996
6020
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
5997
6021
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
5998
6022
  },
5999
- async () => {
6023
+ async (_args, call) => {
6000
6024
  try {
6001
- const ctx = await initializeWorkspaceProject(baseCtx);
6025
+ const ctx = await initializeWorkspaceProject(baseCtx, call);
6002
6026
  const marker = loadProjectMarker(ctx.projectDir);
6003
6027
  if (marker.kind !== "ok")
6004
6028
  throw new Error("init postcondition failed: project marker missing");
@@ -6032,17 +6056,17 @@ function registerHelpTools(server, baseCtx) {
6032
6056
  "help",
6033
6057
  {
6034
6058
  description: "FIRST troubleshooting tool for every Sakupa difficulty. With topic diagnose (default), inspect MCP Roots, cwd, binding and local state without requiring a project or calling the API. Use overview, terminology, or a tool name for complete usage, side effects, parameters and warnings. Only recommend report when help explicitly returns reportRecommended:true.",
6035
- inputSchema: {
6059
+ inputSchema: z4.object({
6036
6060
  topic: z4.enum(HELP_TOPICS).optional().default("diagnose"),
6037
6061
  failedTool: z4.string().optional(),
6038
6062
  errorCode: z4.string().optional(),
6039
6063
  resultCode: z4.string().optional(),
6040
6064
  requestId: z4.string().optional()
6041
- },
6065
+ }),
6042
6066
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
6043
6067
  annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
6044
6068
  },
6045
- async (args) => {
6069
+ async (args, call) => {
6046
6070
  try {
6047
6071
  if (args.topic === "overview") {
6048
6072
  const catalog = Object.fromEntries(
@@ -6104,7 +6128,7 @@ Terminology: ${terminologyText}` : ""),
6104
6128
  nextActions: []
6105
6129
  });
6106
6130
  }
6107
- const diagnosis = await diagnoseProjectBinding(baseCtx);
6131
+ const diagnosis = await diagnoseProjectBinding(baseCtx, call);
6108
6132
  const selected = diagnosis.selectedProjectDir;
6109
6133
  const marker = selected ? loadProjectMarker(selected) : { kind: "absent" };
6110
6134
  const site = selected ? loadSiteFile(selected) : { kind: "absent" };
@@ -6183,18 +6207,18 @@ function registerCredentialTools(server, baseCtx) {
6183
6207
  "rotate",
6184
6208
  {
6185
6209
  description: "Optionally rotate this site management credential. The first call is a read-only preview. Only confirmed:true after explicit user approval installs a locally generated new credential and revokes every previous credential. Rotation is never required to deploy.",
6186
- inputSchema: {
6210
+ inputSchema: z5.object({
6187
6211
  confirmed: z5.boolean().optional().describe(
6188
6212
  "True only after showing the rotate preview and the user explicitly approves revoking every old credential."
6189
6213
  )
6190
- },
6214
+ }),
6191
6215
  outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
6192
6216
  annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
6193
6217
  },
6194
- async (args) => {
6218
+ async (args, call) => {
6195
6219
  let releaseLock;
6196
6220
  try {
6197
- const ctx = await withProjectDir(baseCtx);
6221
+ const ctx = await withProjectDir(baseCtx, call);
6198
6222
  let site = requireSiteFile(ctx);
6199
6223
  const pending = loadCredentialRotation(ctx.projectDir);
6200
6224
  if (pending.kind !== "absent" || args.confirmed === true) {
@@ -6434,23 +6458,7 @@ function createSakupaMcpServer(opts) {
6434
6458
  { instructions: instructionsFor(previewHostPatternFor(opts.apiBaseUrl)) }
6435
6459
  );
6436
6460
  const processCwd = resolve6(opts.projectDir ?? process.cwd());
6437
- const rootsProvider = opts.rootsProvider ?? (async () => {
6438
- const capabilities = server.server.getClientCapabilities();
6439
- if (!capabilities?.roots) return { supported: false, roots: [] };
6440
- try {
6441
- const response = await server.server.listRoots(void 0, {
6442
- timeout: MCP_ROOTS_TIMEOUT_MS,
6443
- maxTotalTimeout: MCP_ROOTS_TIMEOUT_MS
6444
- });
6445
- return { supported: true, roots: response.roots };
6446
- } catch (error) {
6447
- return {
6448
- supported: true,
6449
- roots: [],
6450
- error: error instanceof Error ? error.message : String(error)
6451
- };
6452
- }
6453
- });
6461
+ const rootsProvider = opts.rootsProvider ?? ((call) => readClientRoots(server, call));
6454
6462
  const ctx = {
6455
6463
  client,
6456
6464
  apiBaseUrl: opts.apiBaseUrl,
@@ -6464,6 +6472,38 @@ function createSakupaMcpServer(opts) {
6464
6472
  registerHelpTools(server, ctx);
6465
6473
  return server;
6466
6474
  }
6475
+ async function readClientRoots(server, call) {
6476
+ if (call?.mcpReq.envelope !== void 0) {
6477
+ const envelope = call.mcpReq.envelope;
6478
+ const declared = envelope[CLIENT_CAPABILITIES_META_KEY];
6479
+ if (!declared?.roots) return { supported: false, roots: [] };
6480
+ const answered = inputResponse(call.mcpReq.inputResponses, "roots");
6481
+ if (answered.kind === "roots") return { supported: true, roots: answered.roots };
6482
+ if (call.mcpReq.inputResponses !== void 0) {
6483
+ return {
6484
+ supported: true,
6485
+ roots: [],
6486
+ error: "The client retried without answering the embedded roots/list request."
6487
+ };
6488
+ }
6489
+ throw new McpRootsPending();
6490
+ }
6491
+ const capabilities = server.server.getClientCapabilities();
6492
+ if (!capabilities?.roots) return { supported: false, roots: [] };
6493
+ try {
6494
+ const response = await server.server.listRoots(void 0, {
6495
+ timeout: MCP_ROOTS_TIMEOUT_MS,
6496
+ maxTotalTimeout: MCP_ROOTS_TIMEOUT_MS
6497
+ });
6498
+ return { supported: true, roots: response.roots };
6499
+ } catch (error) {
6500
+ return {
6501
+ supported: true,
6502
+ roots: [],
6503
+ error: error instanceof Error ? error.message : String(error)
6504
+ };
6505
+ }
6506
+ }
6467
6507
  export {
6468
6508
  CLIENT_TYPE,
6469
6509
  FetchTransport,
package/package.json CHANGED
@@ -1,7 +1,18 @@
1
1
  {
2
2
  "name": "@sakupa/mcp",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
+ "mcpName": "io.github.myerwang/sakupa",
4
5
  "description": "Sakupa MCP server: publish AI-made static sites from your AI tool. AI-made pages, live in seconds.",
6
+ "homepage": "https://sakupa.com/manual/",
7
+ "keywords": [
8
+ "mcp",
9
+ "mcp-server",
10
+ "model-context-protocol",
11
+ "static-site",
12
+ "deploy",
13
+ "hosting",
14
+ "sakupa"
15
+ ],
5
16
  "type": "module",
6
17
  "main": "dist/index.js",
7
18
  "bin": {
@@ -23,10 +34,11 @@
23
34
  "node": ">=20"
24
35
  },
25
36
  "dependencies": {
26
- "@modelcontextprotocol/sdk": "^1.12.0",
27
- "zod": "^3.23.8"
37
+ "@modelcontextprotocol/server": "^2.0.0",
38
+ "zod": "^4.2.0"
28
39
  },
29
40
  "devDependencies": {
41
+ "@modelcontextprotocol/client": "^2.0.0",
30
42
  "esbuild": "^0.21.5",
31
43
  "fflate": "0.8.3"
32
44
  }