@sakupa/mcp 1.1.0 → 1.2.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 +111 -66
  2. package/dist/index.js +116 -71
  3. package/package.json +1 -1
package/dist/bin.js CHANGED
@@ -402,7 +402,7 @@ function isFreeSiteAllowanceNetworkReference(value) {
402
402
  }
403
403
 
404
404
  // ../core/dist/domain/version.js
405
- var SAKUPA_MCP_VERSION = "1.1.0";
405
+ var SAKUPA_MCP_VERSION = "1.2.0";
406
406
 
407
407
  // ../core/dist/domain/errors.js
408
408
  var HTTP_STATUS = {
@@ -738,13 +738,15 @@ function previewHostPatternFor(apiBaseUrl) {
738
738
  function loadMcpRuntimeConfig(env = process.env) {
739
739
  const apiBaseUrl = (env["SAKUPA_API_URL"] ?? env["SAKUPA_API_BASE_URL"] ?? DEFAULT_API_BASE_URL).replace(/\/+$/, "");
740
740
  const testAccessToken = env["SAKUPA_TEST_ACCESS_TOKEN"]?.trim() ?? "";
741
+ const projectRoot = env["SAKUPA_PROJECT_ROOT"]?.trim() ?? "";
742
+ const projectRootConfig = projectRoot.length > 0 ? { projectRoot } : {};
741
743
  if (apiBaseUrl === TEST_API_BASE_URL) {
742
744
  if (testAccessToken.length === 0) {
743
745
  throw new Error(
744
746
  "The Sakupa Test API requires SAKUPA_TEST_ACCESS_TOKEN. Anonymous Test access is disabled."
745
747
  );
746
748
  }
747
- return { apiBaseUrl, testAccessToken };
749
+ return { apiBaseUrl, testAccessToken, ...projectRootConfig };
748
750
  }
749
751
  if (apiBaseUrl !== PRODUCTION_API_BASE_URL) {
750
752
  throw new Error(
@@ -756,7 +758,7 @@ function loadMcpRuntimeConfig(env = process.env) {
756
758
  `SAKUPA_TEST_ACCESS_TOKEN may only be used with ${TEST_API_BASE_URL}. Remove it before connecting to any other API.`
757
759
  );
758
760
  }
759
- return { apiBaseUrl };
761
+ return { apiBaseUrl, ...projectRootConfig };
760
762
  }
761
763
  function environmentFor(apiBaseUrl) {
762
764
  if (apiBaseUrl === TEST_API_BASE_URL) return "test";
@@ -2957,7 +2959,7 @@ import {
2957
2959
 
2958
2960
  // src/project-binding.ts
2959
2961
  import { fileURLToPath } from "node:url";
2960
- import { resolve as resolve4 } from "node:path";
2962
+ import { isAbsolute as isAbsolute4, resolve as resolve4 } from "node:path";
2961
2963
  var MCP_ROOTS_TIMEOUT_MS = 5e3;
2962
2964
  var McpRootsPending = class extends Error {
2963
2965
  constructor() {
@@ -2974,10 +2976,11 @@ var ProjectBindingError = class extends Error {
2974
2976
  }
2975
2977
  };
2976
2978
  var ProjectBindingResolver = class {
2977
- constructor(processCwd, rootsProvider, rootsTimeoutMs = MCP_ROOTS_TIMEOUT_MS) {
2979
+ constructor(processCwd, rootsProvider, rootsTimeoutMs = MCP_ROOTS_TIMEOUT_MS, configuredRoot) {
2978
2980
  this.processCwd = processCwd;
2979
2981
  this.rootsProvider = rootsProvider;
2980
2982
  this.rootsTimeoutMs = rootsTimeoutMs;
2983
+ this.configuredRoot = configuredRoot;
2981
2984
  }
2982
2985
  bound;
2983
2986
  boundState;
@@ -3016,28 +3019,41 @@ var ProjectBindingResolver = class {
3016
3019
  throw new ProjectBindingError(inspection.diagnostics);
3017
3020
  }
3018
3021
  const initialized = initializeProject(inspection.initializableRoot);
3019
- this.bound = { ...initialized, bindingSource: "mcp_root" };
3022
+ this.bound = {
3023
+ ...initialized,
3024
+ bindingSource: inspection.initializableSource ?? "mcp_root"
3025
+ };
3020
3026
  this.boundState = boundDiagnostics(
3021
3027
  this.processCwd,
3022
3028
  this.bound,
3023
- { supported: true, roots: [] },
3024
- inspection.diagnostics.rootCandidates
3029
+ { supported: inspection.diagnostics.mcpRootsSupported, roots: [] },
3030
+ inspection.diagnostics.rootCandidates,
3031
+ inspection.diagnostics.configuredProjectRoot
3025
3032
  );
3026
3033
  return this.bound;
3027
3034
  }
3028
3035
  async inspect(forInitialization = false, call) {
3029
3036
  const snapshot = await safeRootsSnapshot(this.rootsProvider, this.rootsTimeoutMs, call);
3030
3037
  const rootCandidates = snapshot.roots.map(inspectRoot);
3038
+ const configured = this.configuredRoot === void 0 ? void 0 : inspectConfiguredRoot(this.configuredRoot);
3039
+ const diagnose = (code, guidance) => diagnostic(code, snapshot, this.processCwd, rootCandidates, guidance, configured);
3040
+ const bound = (selected) => ({
3041
+ selected,
3042
+ diagnostics: boundDiagnostics(
3043
+ this.processCwd,
3044
+ selected,
3045
+ snapshot,
3046
+ rootCandidates,
3047
+ configured
3048
+ )
3049
+ });
3031
3050
  const initializedRoots = rootCandidates.filter(
3032
3051
  (candidate) => candidate.initialized && candidate.path !== void 0
3033
3052
  );
3034
3053
  if (snapshot.supported && snapshot.error) {
3035
3054
  return {
3036
- diagnostics: diagnostic(
3055
+ diagnostics: diagnose(
3037
3056
  "roots_request_failed",
3038
- snapshot,
3039
- this.processCwd,
3040
- rootCandidates,
3041
3057
  "The IDE advertised MCP Roots, but the Roots request failed. Retry help after the IDE finishes loading the workspace. If it persists, restart the MCP connection; do not initialize or deploy from the IDE installation directory."
3042
3058
  )
3043
3059
  };
@@ -3046,19 +3062,12 @@ var ProjectBindingResolver = class {
3046
3062
  const initializedRoot = initializedRoots[0];
3047
3063
  if (!initializedRoot) throw new Error("initialized Root disappeared during resolution");
3048
3064
  const project = resolveLockedProjectRoot(initializedRoot.path);
3049
- const selected = { ...project, bindingSource: "mcp_root" };
3050
- return {
3051
- selected,
3052
- diagnostics: boundDiagnostics(this.processCwd, selected, snapshot, rootCandidates)
3053
- };
3065
+ return bound({ ...project, bindingSource: "mcp_root" });
3054
3066
  }
3055
3067
  if (initializedRoots.length > 1) {
3056
3068
  return {
3057
- diagnostics: diagnostic(
3069
+ diagnostics: diagnose(
3058
3070
  "multiple_initialized_roots",
3059
- snapshot,
3060
- this.processCwd,
3061
- rootCandidates,
3062
3071
  "More than one IDE workspace Root is already initialized for Sakupa. Close the unrelated workspaces and retry help; Sakupa will not guess which site to manage."
3063
3072
  )
3064
3073
  };
@@ -3072,22 +3081,17 @@ var ProjectBindingResolver = class {
3072
3081
  if (!validRoot) throw new Error("workspace Root disappeared during initialization");
3073
3082
  return {
3074
3083
  initializableRoot: validRoot.path,
3075
- diagnostics: diagnostic(
3084
+ initializableSource: "mcp_root",
3085
+ diagnostics: diagnose(
3076
3086
  "workspace_not_initialized",
3077
- snapshot,
3078
- this.processCwd,
3079
- rootCandidates,
3080
3087
  `The active MCP workspace ${validRoot.path} is ready to initialize.`
3081
3088
  )
3082
3089
  };
3083
3090
  }
3084
3091
  if (validRoots.length > 1) {
3085
3092
  return {
3086
- diagnostics: diagnostic(
3093
+ diagnostics: diagnose(
3087
3094
  "multiple_uninitialized_roots",
3088
- snapshot,
3089
- this.processCwd,
3090
- rootCandidates,
3091
3095
  "The IDE exposes multiple uninitialized workspace Roots. Open only the intended project before calling init; Sakupa will not choose a directory for the user."
3092
3096
  )
3093
3097
  };
@@ -3097,54 +3101,59 @@ var ProjectBindingResolver = class {
3097
3101
  const validRoot = validRoots[0];
3098
3102
  if (!validRoot) throw new Error("workspace Root disappeared during diagnosis");
3099
3103
  return {
3100
- diagnostics: diagnostic(
3104
+ diagnostics: diagnose(
3101
3105
  "workspace_not_initialized",
3102
- snapshot,
3103
- this.processCwd,
3104
- rootCandidates,
3105
3106
  `The IDE workspace ${validRoot.path} is not initialized. Call init with no path arguments; it will create .sakupa directly in that workspace Root.`
3106
3107
  )
3107
3108
  };
3108
3109
  }
3109
3110
  if (snapshot.supported && validRoots.length > 1) {
3110
3111
  return {
3111
- diagnostics: diagnostic(
3112
+ diagnostics: diagnose(
3112
3113
  "multiple_uninitialized_roots",
3113
- snapshot,
3114
- this.processCwd,
3115
- rootCandidates,
3116
3114
  "The IDE exposes multiple uninitialized workspace Roots. Open only the intended project, then call init. Sakupa will not guess a project directory."
3117
3115
  )
3118
3116
  };
3119
3117
  }
3118
+ if (configured) {
3119
+ if (configured.problem !== void 0 || configured.path === void 0) {
3120
+ return {
3121
+ diagnostics: diagnose(
3122
+ "invalid_configured_root",
3123
+ `SAKUPA_PROJECT_ROOT is set to ${configured.configured} but it is not usable: ${configured.problem ?? "unknown problem"}. Fix the MCP server configuration (an absolute path to an existing project directory) and retry help; Sakupa will not fall back to another directory.`
3124
+ )
3125
+ };
3126
+ }
3127
+ if (configured.initialized) {
3128
+ const project = resolveLockedProjectRoot(configured.path);
3129
+ return bound({ ...project, bindingSource: "configured_root" });
3130
+ }
3131
+ return {
3132
+ ...forInitialization ? { initializableRoot: configured.path, initializableSource: "configured_root" } : {},
3133
+ diagnostics: diagnose(
3134
+ "workspace_not_initialized",
3135
+ `The configured project root ${configured.path} (SAKUPA_PROJECT_ROOT) is not initialized. Call init with no path arguments; it will create .sakupa directly there.`
3136
+ )
3137
+ };
3138
+ }
3120
3139
  if (snapshot.supported) {
3121
3140
  return {
3122
- diagnostics: diagnostic(
3141
+ diagnostics: diagnose(
3123
3142
  "workspace_not_initialized",
3124
- snapshot,
3125
- this.processCwd,
3126
- rootCandidates,
3127
3143
  "The IDE did not expose one usable file workspace Root. Open exactly one local project workspace, then retry help before calling init or deploy."
3128
3144
  )
3129
3145
  };
3130
3146
  }
3131
3147
  try {
3132
3148
  const cwdProject = resolveLockedProjectRoot(this.processCwd);
3133
- const selected = { ...cwdProject, bindingSource: "process_cwd" };
3134
- return {
3135
- selected,
3136
- diagnostics: boundDiagnostics(this.processCwd, selected, snapshot, rootCandidates)
3137
- };
3149
+ return bound({ ...cwdProject, bindingSource: "process_cwd" });
3138
3150
  } catch {
3139
3151
  }
3140
3152
  const cwdProblem = inspectDirectory(this.processCwd);
3141
3153
  return {
3142
- diagnostics: diagnostic(
3154
+ diagnostics: diagnose(
3143
3155
  cwdProblem.problem ? "invalid_process_cwd" : "process_cwd_is_not_workspace",
3144
- snapshot,
3145
- this.processCwd,
3146
- rootCandidates,
3147
- "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."
3156
+ "This IDE did not provide MCP Roots, SAKUPA_PROJECT_ROOT is not configured, 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, or the MCP server configuration may set SAKUPA_PROJECT_ROOT to the absolute project path. Never ask the user to run it."
3148
3157
  )
3149
3158
  };
3150
3159
  }
@@ -3157,11 +3166,7 @@ function fileRootUriToPath(uri, windows = process.platform === "win32") {
3157
3166
  async function safeRootsSnapshot(provider, timeoutMs = MCP_ROOTS_TIMEOUT_MS, call) {
3158
3167
  if (!provider) return { supported: false, roots: [] };
3159
3168
  try {
3160
- return await withOperationTimeout(
3161
- "MCP Roots request",
3162
- timeoutMs,
3163
- () => provider(call)
3164
- );
3169
+ return await withOperationTimeout("MCP Roots request", timeoutMs, () => provider(call));
3165
3170
  } catch (error) {
3166
3171
  if (error instanceof McpRootsPending) throw error;
3167
3172
  return {
@@ -3191,6 +3196,31 @@ function inspectRoot(root) {
3191
3196
  };
3192
3197
  }
3193
3198
  }
3199
+ function inspectConfiguredRoot(configured) {
3200
+ if (!isAbsolute4(configured)) {
3201
+ return {
3202
+ configured,
3203
+ initialized: false,
3204
+ problem: "the value must be an absolute path"
3205
+ };
3206
+ }
3207
+ try {
3208
+ const path = canonicalProjectDirectory(configured);
3209
+ const marker = loadProjectMarker(path);
3210
+ return {
3211
+ configured,
3212
+ path,
3213
+ initialized: marker.kind === "ok",
3214
+ ...marker.kind === "corrupted" ? { problem: marker.problem } : {}
3215
+ };
3216
+ } catch (error) {
3217
+ return {
3218
+ configured,
3219
+ initialized: false,
3220
+ problem: error instanceof Error ? error.message : String(error)
3221
+ };
3222
+ }
3223
+ }
3194
3224
  function inspectDirectory(path) {
3195
3225
  try {
3196
3226
  return { path: canonicalProjectDirectory(resolve4(path)) };
@@ -3198,22 +3228,24 @@ function inspectDirectory(path) {
3198
3228
  return { problem: error instanceof Error ? error.message : String(error) };
3199
3229
  }
3200
3230
  }
3201
- function diagnostic(diagnosisCode, snapshot, processCwd, rootCandidates, guidance) {
3231
+ function diagnostic(diagnosisCode, snapshot, processCwd, rootCandidates, guidance, configured) {
3202
3232
  return {
3203
3233
  diagnosisCode,
3204
3234
  mcpRootsSupported: snapshot.supported,
3205
3235
  processCwd,
3206
3236
  rootCandidates,
3237
+ ...configured ? { configuredProjectRoot: configured } : {},
3207
3238
  guidance,
3208
3239
  reportRecommended: false
3209
3240
  };
3210
3241
  }
3211
- function boundDiagnostics(processCwd, selected, snapshot = { supported: false, roots: [] }, rootCandidates = []) {
3242
+ function boundDiagnostics(processCwd, selected, snapshot = { supported: false, roots: [] }, rootCandidates = [], configured) {
3212
3243
  return {
3213
3244
  diagnosisCode: "project_bound",
3214
3245
  mcpRootsSupported: snapshot.supported,
3215
3246
  processCwd,
3216
3247
  rootCandidates,
3248
+ ...configured ? { configuredProjectRoot: configured } : {},
3217
3249
  selectedProjectDir: selected.projectDir,
3218
3250
  bindingSource: selected.bindingSource,
3219
3251
  guidance: `Sakupa is locked to ${selected.projectDir} from ${selected.bindingSource}.`,
@@ -3432,7 +3464,12 @@ function resolverFor(ctx) {
3432
3464
  if (ctx.projectBinding) return ctx.projectBinding;
3433
3465
  let resolver = fallbackResolvers.get(ctx);
3434
3466
  if (!resolver) {
3435
- resolver = new ProjectBindingResolver(ctx.projectDir, ctx.rootsProvider);
3467
+ resolver = new ProjectBindingResolver(
3468
+ ctx.projectDir,
3469
+ ctx.rootsProvider,
3470
+ MCP_ROOTS_TIMEOUT_MS,
3471
+ ctx.configuredProjectRoot
3472
+ );
3436
3473
  fallbackResolvers.set(ctx, resolver);
3437
3474
  }
3438
3475
  return resolver;
@@ -5698,7 +5735,7 @@ var TOOL_MANUALS = {
5698
5735
  init: {
5699
5736
  purpose: "Initialize the active IDE workspace as one Sakupa project.",
5700
5737
  sideEffects: "Creates only .sakupa/project.json locally; no API call, site or charge.",
5701
- preconditions: "Exactly one usable MCP workspace Root. If Roots are unavailable, help may authorize the AI to use CLI init.",
5738
+ preconditions: "Exactly one usable MCP workspace Root, or the SAKUPA_PROJECT_ROOT directory configured for this server when the client provides no Roots. If neither exists, help may authorize the AI to use CLI init.",
5702
5739
  parameterNames: [],
5703
5740
  parameters: "No parameters and no path argument.",
5704
5741
  warnings: [
@@ -6406,11 +6443,13 @@ language. Keep option IDs, tool names, exact arguments, URLs, field names and co
6406
6443
  unchanged.
6407
6444
 
6408
6445
  Project directory contract: before the first deploy or a new recovery, CALL the init MCP tool with
6409
- NO path argument. init uses the IDE's exact MCP Root and creates the non-secret
6446
+ NO path argument. init uses the IDE's exact MCP Root \u2014 or, when the client provides no Roots, the
6447
+ SAKUPA_PROJECT_ROOT directory configured for this MCP server \u2014 and creates the non-secret
6410
6448
  .sakupa/project.json directly there. Do not merely print installation or CLI instructions when the
6411
- init tool is available. Only after help confirms that the client does not provide MCP Roots may the
6412
- AI itself use the CLI command "npx -y @sakupa/mcp@latest init" as a fallback; never ask the user to
6413
- run it. The CLI also accepts NO path argument. ONE MCP process = ONE Roots-first locked project = ONE site.
6449
+ init tool is available. Only after help confirms that the client provides neither MCP Roots nor
6450
+ SAKUPA_PROJECT_ROOT may the AI itself use the CLI command "npx -y @sakupa/mcp@latest init" as a
6451
+ fallback; never ask the user to run it. The CLI also accepts NO path argument. ONE MCP process = ONE
6452
+ locked project = ONE site.
6414
6453
  Site tools do not accept projectDir and cannot select another root; help, plans and report preview
6415
6454
  and public_recovery portal remain project-independent.
6416
6455
  Sakupa stores .sakupa/site.json and recovery state only in the locked directory; it never uses
@@ -6496,7 +6535,13 @@ function createSakupaMcpServer(opts) {
6496
6535
  apiBaseUrl: opts.apiBaseUrl,
6497
6536
  projectDir: processCwd,
6498
6537
  rootsProvider,
6499
- projectBinding: new ProjectBindingResolver(processCwd, rootsProvider)
6538
+ ...opts.projectRoot !== void 0 ? { configuredProjectRoot: opts.projectRoot } : {},
6539
+ projectBinding: new ProjectBindingResolver(
6540
+ processCwd,
6541
+ rootsProvider,
6542
+ MCP_ROOTS_TIMEOUT_MS,
6543
+ opts.projectRoot
6544
+ )
6500
6545
  };
6501
6546
  registerTools(server, ctx);
6502
6547
  registerBillingTools(server, ctx);
@@ -6561,7 +6606,7 @@ async function main() {
6561
6606
  onerror: (error) => console.error("[sakupa-mcp] transport error:", error.message)
6562
6607
  });
6563
6608
  console.error(
6564
- `[sakupa-mcp] v${MCP_VERSION} serving stdio (api: ${config.apiBaseUrl}; process cwd fallback: ${process.cwd()}; MCP Roots preferred)`
6609
+ `[sakupa-mcp] v${MCP_VERSION} serving stdio (api: ${config.apiBaseUrl}; configured root: ${config.projectRoot ?? "none"}; process cwd fallback: ${process.cwd()}; MCP Roots preferred)`
6565
6610
  );
6566
6611
  }
6567
6612
  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.1.0";
150
+ var SAKUPA_MCP_VERSION = "1.2.0";
151
151
 
152
152
  // ../core/dist/domain/errors.js
153
153
  var HTTP_STATUS = {
@@ -487,13 +487,15 @@ function previewHostPatternFor(apiBaseUrl) {
487
487
  function loadMcpRuntimeConfig(env = process.env) {
488
488
  const apiBaseUrl = (env["SAKUPA_API_URL"] ?? env["SAKUPA_API_BASE_URL"] ?? DEFAULT_API_BASE_URL).replace(/\/+$/, "");
489
489
  const testAccessToken = env["SAKUPA_TEST_ACCESS_TOKEN"]?.trim() ?? "";
490
+ const projectRoot = env["SAKUPA_PROJECT_ROOT"]?.trim() ?? "";
491
+ const projectRootConfig = projectRoot.length > 0 ? { projectRoot } : {};
490
492
  if (apiBaseUrl === TEST_API_BASE_URL) {
491
493
  if (testAccessToken.length === 0) {
492
494
  throw new Error(
493
495
  "The Sakupa Test API requires SAKUPA_TEST_ACCESS_TOKEN. Anonymous Test access is disabled."
494
496
  );
495
497
  }
496
- return { apiBaseUrl, testAccessToken };
498
+ return { apiBaseUrl, testAccessToken, ...projectRootConfig };
497
499
  }
498
500
  if (apiBaseUrl !== PRODUCTION_API_BASE_URL) {
499
501
  throw new Error(
@@ -505,7 +507,7 @@ function loadMcpRuntimeConfig(env = process.env) {
505
507
  `SAKUPA_TEST_ACCESS_TOKEN may only be used with ${TEST_API_BASE_URL}. Remove it before connecting to any other API.`
506
508
  );
507
509
  }
508
- return { apiBaseUrl };
510
+ return { apiBaseUrl, ...projectRootConfig };
509
511
  }
510
512
  function environmentFor(apiBaseUrl) {
511
513
  if (apiBaseUrl === TEST_API_BASE_URL) return "test";
@@ -1543,7 +1545,7 @@ import {
1543
1545
 
1544
1546
  // src/project-binding.ts
1545
1547
  import { fileURLToPath } from "node:url";
1546
- import { resolve as resolve3 } from "node:path";
1548
+ import { isAbsolute as isAbsolute2, resolve as resolve3 } from "node:path";
1547
1549
 
1548
1550
  // src/project-root.ts
1549
1551
  import { randomUUID as randomUUID2 } from "node:crypto";
@@ -1783,10 +1785,11 @@ var ProjectBindingError = class extends Error {
1783
1785
  }
1784
1786
  };
1785
1787
  var ProjectBindingResolver = class {
1786
- constructor(processCwd, rootsProvider, rootsTimeoutMs = MCP_ROOTS_TIMEOUT_MS) {
1788
+ constructor(processCwd, rootsProvider, rootsTimeoutMs = MCP_ROOTS_TIMEOUT_MS, configuredRoot) {
1787
1789
  this.processCwd = processCwd;
1788
1790
  this.rootsProvider = rootsProvider;
1789
1791
  this.rootsTimeoutMs = rootsTimeoutMs;
1792
+ this.configuredRoot = configuredRoot;
1790
1793
  }
1791
1794
  bound;
1792
1795
  boundState;
@@ -1825,28 +1828,41 @@ var ProjectBindingResolver = class {
1825
1828
  throw new ProjectBindingError(inspection.diagnostics);
1826
1829
  }
1827
1830
  const initialized = initializeProject(inspection.initializableRoot);
1828
- this.bound = { ...initialized, bindingSource: "mcp_root" };
1831
+ this.bound = {
1832
+ ...initialized,
1833
+ bindingSource: inspection.initializableSource ?? "mcp_root"
1834
+ };
1829
1835
  this.boundState = boundDiagnostics(
1830
1836
  this.processCwd,
1831
1837
  this.bound,
1832
- { supported: true, roots: [] },
1833
- inspection.diagnostics.rootCandidates
1838
+ { supported: inspection.diagnostics.mcpRootsSupported, roots: [] },
1839
+ inspection.diagnostics.rootCandidates,
1840
+ inspection.diagnostics.configuredProjectRoot
1834
1841
  );
1835
1842
  return this.bound;
1836
1843
  }
1837
1844
  async inspect(forInitialization = false, call) {
1838
1845
  const snapshot = await safeRootsSnapshot(this.rootsProvider, this.rootsTimeoutMs, call);
1839
1846
  const rootCandidates = snapshot.roots.map(inspectRoot);
1847
+ const configured = this.configuredRoot === void 0 ? void 0 : inspectConfiguredRoot(this.configuredRoot);
1848
+ const diagnose = (code, guidance) => diagnostic(code, snapshot, this.processCwd, rootCandidates, guidance, configured);
1849
+ const bound = (selected) => ({
1850
+ selected,
1851
+ diagnostics: boundDiagnostics(
1852
+ this.processCwd,
1853
+ selected,
1854
+ snapshot,
1855
+ rootCandidates,
1856
+ configured
1857
+ )
1858
+ });
1840
1859
  const initializedRoots = rootCandidates.filter(
1841
1860
  (candidate) => candidate.initialized && candidate.path !== void 0
1842
1861
  );
1843
1862
  if (snapshot.supported && snapshot.error) {
1844
1863
  return {
1845
- diagnostics: diagnostic(
1864
+ diagnostics: diagnose(
1846
1865
  "roots_request_failed",
1847
- snapshot,
1848
- this.processCwd,
1849
- rootCandidates,
1850
1866
  "The IDE advertised MCP Roots, but the Roots request failed. Retry help after the IDE finishes loading the workspace. If it persists, restart the MCP connection; do not initialize or deploy from the IDE installation directory."
1851
1867
  )
1852
1868
  };
@@ -1855,19 +1871,12 @@ var ProjectBindingResolver = class {
1855
1871
  const initializedRoot = initializedRoots[0];
1856
1872
  if (!initializedRoot) throw new Error("initialized Root disappeared during resolution");
1857
1873
  const project = resolveLockedProjectRoot(initializedRoot.path);
1858
- const selected = { ...project, bindingSource: "mcp_root" };
1859
- return {
1860
- selected,
1861
- diagnostics: boundDiagnostics(this.processCwd, selected, snapshot, rootCandidates)
1862
- };
1874
+ return bound({ ...project, bindingSource: "mcp_root" });
1863
1875
  }
1864
1876
  if (initializedRoots.length > 1) {
1865
1877
  return {
1866
- diagnostics: diagnostic(
1878
+ diagnostics: diagnose(
1867
1879
  "multiple_initialized_roots",
1868
- snapshot,
1869
- this.processCwd,
1870
- rootCandidates,
1871
1880
  "More than one IDE workspace Root is already initialized for Sakupa. Close the unrelated workspaces and retry help; Sakupa will not guess which site to manage."
1872
1881
  )
1873
1882
  };
@@ -1881,22 +1890,17 @@ var ProjectBindingResolver = class {
1881
1890
  if (!validRoot) throw new Error("workspace Root disappeared during initialization");
1882
1891
  return {
1883
1892
  initializableRoot: validRoot.path,
1884
- diagnostics: diagnostic(
1893
+ initializableSource: "mcp_root",
1894
+ diagnostics: diagnose(
1885
1895
  "workspace_not_initialized",
1886
- snapshot,
1887
- this.processCwd,
1888
- rootCandidates,
1889
1896
  `The active MCP workspace ${validRoot.path} is ready to initialize.`
1890
1897
  )
1891
1898
  };
1892
1899
  }
1893
1900
  if (validRoots.length > 1) {
1894
1901
  return {
1895
- diagnostics: diagnostic(
1902
+ diagnostics: diagnose(
1896
1903
  "multiple_uninitialized_roots",
1897
- snapshot,
1898
- this.processCwd,
1899
- rootCandidates,
1900
1904
  "The IDE exposes multiple uninitialized workspace Roots. Open only the intended project before calling init; Sakupa will not choose a directory for the user."
1901
1905
  )
1902
1906
  };
@@ -1906,54 +1910,59 @@ var ProjectBindingResolver = class {
1906
1910
  const validRoot = validRoots[0];
1907
1911
  if (!validRoot) throw new Error("workspace Root disappeared during diagnosis");
1908
1912
  return {
1909
- diagnostics: diagnostic(
1913
+ diagnostics: diagnose(
1910
1914
  "workspace_not_initialized",
1911
- snapshot,
1912
- this.processCwd,
1913
- rootCandidates,
1914
1915
  `The IDE workspace ${validRoot.path} is not initialized. Call init with no path arguments; it will create .sakupa directly in that workspace Root.`
1915
1916
  )
1916
1917
  };
1917
1918
  }
1918
1919
  if (snapshot.supported && validRoots.length > 1) {
1919
1920
  return {
1920
- diagnostics: diagnostic(
1921
+ diagnostics: diagnose(
1921
1922
  "multiple_uninitialized_roots",
1922
- snapshot,
1923
- this.processCwd,
1924
- rootCandidates,
1925
1923
  "The IDE exposes multiple uninitialized workspace Roots. Open only the intended project, then call init. Sakupa will not guess a project directory."
1926
1924
  )
1927
1925
  };
1928
1926
  }
1927
+ if (configured) {
1928
+ if (configured.problem !== void 0 || configured.path === void 0) {
1929
+ return {
1930
+ diagnostics: diagnose(
1931
+ "invalid_configured_root",
1932
+ `SAKUPA_PROJECT_ROOT is set to ${configured.configured} but it is not usable: ${configured.problem ?? "unknown problem"}. Fix the MCP server configuration (an absolute path to an existing project directory) and retry help; Sakupa will not fall back to another directory.`
1933
+ )
1934
+ };
1935
+ }
1936
+ if (configured.initialized) {
1937
+ const project = resolveLockedProjectRoot(configured.path);
1938
+ return bound({ ...project, bindingSource: "configured_root" });
1939
+ }
1940
+ return {
1941
+ ...forInitialization ? { initializableRoot: configured.path, initializableSource: "configured_root" } : {},
1942
+ diagnostics: diagnose(
1943
+ "workspace_not_initialized",
1944
+ `The configured project root ${configured.path} (SAKUPA_PROJECT_ROOT) is not initialized. Call init with no path arguments; it will create .sakupa directly there.`
1945
+ )
1946
+ };
1947
+ }
1929
1948
  if (snapshot.supported) {
1930
1949
  return {
1931
- diagnostics: diagnostic(
1950
+ diagnostics: diagnose(
1932
1951
  "workspace_not_initialized",
1933
- snapshot,
1934
- this.processCwd,
1935
- rootCandidates,
1936
1952
  "The IDE did not expose one usable file workspace Root. Open exactly one local project workspace, then retry help before calling init or deploy."
1937
1953
  )
1938
1954
  };
1939
1955
  }
1940
1956
  try {
1941
1957
  const cwdProject = resolveLockedProjectRoot(this.processCwd);
1942
- const selected = { ...cwdProject, bindingSource: "process_cwd" };
1943
- return {
1944
- selected,
1945
- diagnostics: boundDiagnostics(this.processCwd, selected, snapshot, rootCandidates)
1946
- };
1958
+ return bound({ ...cwdProject, bindingSource: "process_cwd" });
1947
1959
  } catch {
1948
1960
  }
1949
1961
  const cwdProblem = inspectDirectory(this.processCwd);
1950
1962
  return {
1951
- diagnostics: diagnostic(
1963
+ diagnostics: diagnose(
1952
1964
  cwdProblem.problem ? "invalid_process_cwd" : "process_cwd_is_not_workspace",
1953
- snapshot,
1954
- this.processCwd,
1955
- rootCandidates,
1956
- "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."
1965
+ "This IDE did not provide MCP Roots, SAKUPA_PROJECT_ROOT is not configured, 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, or the MCP server configuration may set SAKUPA_PROJECT_ROOT to the absolute project path. Never ask the user to run it."
1957
1966
  )
1958
1967
  };
1959
1968
  }
@@ -1966,11 +1975,7 @@ function fileRootUriToPath(uri, windows = process.platform === "win32") {
1966
1975
  async function safeRootsSnapshot(provider, timeoutMs = MCP_ROOTS_TIMEOUT_MS, call) {
1967
1976
  if (!provider) return { supported: false, roots: [] };
1968
1977
  try {
1969
- return await withOperationTimeout(
1970
- "MCP Roots request",
1971
- timeoutMs,
1972
- () => provider(call)
1973
- );
1978
+ return await withOperationTimeout("MCP Roots request", timeoutMs, () => provider(call));
1974
1979
  } catch (error) {
1975
1980
  if (error instanceof McpRootsPending) throw error;
1976
1981
  return {
@@ -2000,6 +2005,31 @@ function inspectRoot(root) {
2000
2005
  };
2001
2006
  }
2002
2007
  }
2008
+ function inspectConfiguredRoot(configured) {
2009
+ if (!isAbsolute2(configured)) {
2010
+ return {
2011
+ configured,
2012
+ initialized: false,
2013
+ problem: "the value must be an absolute path"
2014
+ };
2015
+ }
2016
+ try {
2017
+ const path = canonicalProjectDirectory(configured);
2018
+ const marker = loadProjectMarker(path);
2019
+ return {
2020
+ configured,
2021
+ path,
2022
+ initialized: marker.kind === "ok",
2023
+ ...marker.kind === "corrupted" ? { problem: marker.problem } : {}
2024
+ };
2025
+ } catch (error) {
2026
+ return {
2027
+ configured,
2028
+ initialized: false,
2029
+ problem: error instanceof Error ? error.message : String(error)
2030
+ };
2031
+ }
2032
+ }
2003
2033
  function inspectDirectory(path) {
2004
2034
  try {
2005
2035
  return { path: canonicalProjectDirectory(resolve3(path)) };
@@ -2007,22 +2037,24 @@ function inspectDirectory(path) {
2007
2037
  return { problem: error instanceof Error ? error.message : String(error) };
2008
2038
  }
2009
2039
  }
2010
- function diagnostic(diagnosisCode, snapshot, processCwd, rootCandidates, guidance) {
2040
+ function diagnostic(diagnosisCode, snapshot, processCwd, rootCandidates, guidance, configured) {
2011
2041
  return {
2012
2042
  diagnosisCode,
2013
2043
  mcpRootsSupported: snapshot.supported,
2014
2044
  processCwd,
2015
2045
  rootCandidates,
2046
+ ...configured ? { configuredProjectRoot: configured } : {},
2016
2047
  guidance,
2017
2048
  reportRecommended: false
2018
2049
  };
2019
2050
  }
2020
- function boundDiagnostics(processCwd, selected, snapshot = { supported: false, roots: [] }, rootCandidates = []) {
2051
+ function boundDiagnostics(processCwd, selected, snapshot = { supported: false, roots: [] }, rootCandidates = [], configured) {
2021
2052
  return {
2022
2053
  diagnosisCode: "project_bound",
2023
2054
  mcpRootsSupported: snapshot.supported,
2024
2055
  processCwd,
2025
2056
  rootCandidates,
2057
+ ...configured ? { configuredProjectRoot: configured } : {},
2026
2058
  selectedProjectDir: selected.projectDir,
2027
2059
  bindingSource: selected.bindingSource,
2028
2060
  guidance: `Sakupa is locked to ${selected.projectDir} from ${selected.bindingSource}.`,
@@ -2241,7 +2273,12 @@ function resolverFor(ctx) {
2241
2273
  if (ctx.projectBinding) return ctx.projectBinding;
2242
2274
  let resolver = fallbackResolvers.get(ctx);
2243
2275
  if (!resolver) {
2244
- resolver = new ProjectBindingResolver(ctx.projectDir, ctx.rootsProvider);
2276
+ resolver = new ProjectBindingResolver(
2277
+ ctx.projectDir,
2278
+ ctx.rootsProvider,
2279
+ MCP_ROOTS_TIMEOUT_MS,
2280
+ ctx.configuredProjectRoot
2281
+ );
2245
2282
  fallbackResolvers.set(ctx, resolver);
2246
2283
  }
2247
2284
  return resolver;
@@ -2418,7 +2455,7 @@ import { z as z2 } from "zod";
2418
2455
  // src/recovery-archive.ts
2419
2456
  import { existsSync as existsSync3, realpathSync as realpathSync2 } from "node:fs";
2420
2457
  import { mkdtemp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
2421
- import { dirname as dirname2, isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as resolve4, sep as sep3 } from "node:path";
2458
+ import { dirname as dirname2, isAbsolute as isAbsolute3, join as join4, relative as relative2, resolve as resolve4, sep as sep3 } from "node:path";
2422
2459
 
2423
2460
  // ../../node_modules/fflate/esm/index.mjs
2424
2461
  import { createRequire } from "module";
@@ -2905,13 +2942,13 @@ function unzipSync(data, opts) {
2905
2942
 
2906
2943
  // src/recovery-archive.ts
2907
2944
  function safeOutputPath(projectDir, outputDir) {
2908
- if (outputDir.length === 0 || isAbsolute2(outputDir)) {
2945
+ if (outputDir.length === 0 || isAbsolute3(outputDir)) {
2909
2946
  throw new SakupaError("invalid_request", "Recovery outputDir must be a relative directory");
2910
2947
  }
2911
2948
  const root = realpathSync2(resolve4(projectDir));
2912
2949
  const target = resolve4(root, outputDir);
2913
2950
  const rel = relative2(root, target);
2914
- if (rel === "" || rel === ".." || rel.startsWith(`..${sep3}`) || isAbsolute2(rel)) {
2951
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep3}`) || isAbsolute3(rel)) {
2915
2952
  throw new SakupaError("invalid_request", "Recovery outputDir must stay inside projectDir");
2916
2953
  }
2917
2954
  if (rel === ".sakupa" || rel.startsWith(`.sakupa${sep3}`)) {
@@ -2926,7 +2963,7 @@ function safeOutputPath(projectDir, outputDir) {
2926
2963
  const physicalAncestor = realpathSync2(existingAncestor);
2927
2964
  const physicalTarget = resolve4(physicalAncestor, relative2(existingAncestor, target));
2928
2965
  const physicalRel = relative2(root, physicalTarget);
2929
- if (physicalRel === ".." || physicalRel.startsWith(`..${sep3}`) || isAbsolute2(physicalRel)) {
2966
+ if (physicalRel === ".." || physicalRel.startsWith(`..${sep3}`) || isAbsolute3(physicalRel)) {
2930
2967
  throw new SakupaError(
2931
2968
  "invalid_request",
2932
2969
  "Recovery outputDir resolves through a symlink outside projectDir"
@@ -3292,7 +3329,7 @@ import {
3292
3329
  writeFileSync as writeFileSync5
3293
3330
  } from "node:fs";
3294
3331
  import { createHash } from "node:crypto";
3295
- import { dirname as dirname5, isAbsolute as isAbsolute3, join as join7 } from "node:path";
3332
+ import { dirname as dirname5, isAbsolute as isAbsolute4, join as join7 } from "node:path";
3296
3333
  var HANDOFF_LOCK_TTL_MS = 15 * 60 * 1e3;
3297
3334
  function normalizeSiteUrl(raw) {
3298
3335
  const url = new URL(raw);
@@ -3319,7 +3356,7 @@ function resolveReusableSite(rawUrl, currentProjectDir, nowMs, apiBaseUrl) {
3319
3356
  throw new Error(`More than one local free-site record matches ${siteUrl}.`);
3320
3357
  const record = matches2[0];
3321
3358
  if (!record) throw new Error("The selected existing free site disappeared during resolution.");
3322
- if (!isAbsolute3(record.projectDir)) {
3359
+ if (!isAbsolute4(record.projectDir)) {
3323
3360
  throw new Error("The selected free-site project path is not absolute; refusing cwd lookup.");
3324
3361
  }
3325
3362
  const sourceProjectDir = canonicalProjectDirectory(record.projectDir);
@@ -5817,7 +5854,7 @@ var TOOL_MANUALS = {
5817
5854
  init: {
5818
5855
  purpose: "Initialize the active IDE workspace as one Sakupa project.",
5819
5856
  sideEffects: "Creates only .sakupa/project.json locally; no API call, site or charge.",
5820
- preconditions: "Exactly one usable MCP workspace Root. If Roots are unavailable, help may authorize the AI to use CLI init.",
5857
+ preconditions: "Exactly one usable MCP workspace Root, or the SAKUPA_PROJECT_ROOT directory configured for this server when the client provides no Roots. If neither exists, help may authorize the AI to use CLI init.",
5821
5858
  parameterNames: [],
5822
5859
  parameters: "No parameters and no path argument.",
5823
5860
  warnings: [
@@ -6374,11 +6411,13 @@ language. Keep option IDs, tool names, exact arguments, URLs, field names and co
6374
6411
  unchanged.
6375
6412
 
6376
6413
  Project directory contract: before the first deploy or a new recovery, CALL the init MCP tool with
6377
- NO path argument. init uses the IDE's exact MCP Root and creates the non-secret
6414
+ NO path argument. init uses the IDE's exact MCP Root \u2014 or, when the client provides no Roots, the
6415
+ SAKUPA_PROJECT_ROOT directory configured for this MCP server \u2014 and creates the non-secret
6378
6416
  .sakupa/project.json directly there. Do not merely print installation or CLI instructions when the
6379
- init tool is available. Only after help confirms that the client does not provide MCP Roots may the
6380
- AI itself use the CLI command "npx -y @sakupa/mcp@latest init" as a fallback; never ask the user to
6381
- run it. The CLI also accepts NO path argument. ONE MCP process = ONE Roots-first locked project = ONE site.
6417
+ init tool is available. Only after help confirms that the client provides neither MCP Roots nor
6418
+ SAKUPA_PROJECT_ROOT may the AI itself use the CLI command "npx -y @sakupa/mcp@latest init" as a
6419
+ fallback; never ask the user to run it. The CLI also accepts NO path argument. ONE MCP process = ONE
6420
+ locked project = ONE site.
6382
6421
  Site tools do not accept projectDir and cannot select another root; help, plans and report preview
6383
6422
  and public_recovery portal remain project-independent.
6384
6423
  Sakupa stores .sakupa/site.json and recovery state only in the locked directory; it never uses
@@ -6464,7 +6503,13 @@ function createSakupaMcpServer(opts) {
6464
6503
  apiBaseUrl: opts.apiBaseUrl,
6465
6504
  projectDir: processCwd,
6466
6505
  rootsProvider,
6467
- projectBinding: new ProjectBindingResolver(processCwd, rootsProvider)
6506
+ ...opts.projectRoot !== void 0 ? { configuredProjectRoot: opts.projectRoot } : {},
6507
+ projectBinding: new ProjectBindingResolver(
6508
+ processCwd,
6509
+ rootsProvider,
6510
+ MCP_ROOTS_TIMEOUT_MS,
6511
+ opts.projectRoot
6512
+ )
6468
6513
  };
6469
6514
  registerTools(server, ctx);
6470
6515
  registerBillingTools(server, ctx);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakupa/mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "mcpName": "io.github.myerwang/sakupa",
5
5
  "description": "Sakupa MCP server: publish AI-made static sites from your AI tool. AI-made pages, live in seconds.",
6
6
  "homepage": "https://sakupa.com/manual/",