@sakupa/mcp 1.0.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.
- package/dist/bin.js +253 -163
- package/dist/index.js +248 -163
- package/package.json +15 -3
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.
|
|
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";
|
|
@@ -1536,9 +1538,14 @@ async function analyzeProject(projectDir, opts = {}) {
|
|
|
1536
1538
|
};
|
|
1537
1539
|
}
|
|
1538
1540
|
|
|
1541
|
+
// src/tools/context.ts
|
|
1542
|
+
import {
|
|
1543
|
+
inputRequired
|
|
1544
|
+
} from "@modelcontextprotocol/server";
|
|
1545
|
+
|
|
1539
1546
|
// src/project-binding.ts
|
|
1540
1547
|
import { fileURLToPath } from "node:url";
|
|
1541
|
-
import { resolve as resolve3 } from "node:path";
|
|
1548
|
+
import { isAbsolute as isAbsolute2, resolve as resolve3 } from "node:path";
|
|
1542
1549
|
|
|
1543
1550
|
// src/project-root.ts
|
|
1544
1551
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -1763,6 +1770,12 @@ function writeMarkerAtomically(projectDir, marker) {
|
|
|
1763
1770
|
|
|
1764
1771
|
// src/project-binding.ts
|
|
1765
1772
|
var MCP_ROOTS_TIMEOUT_MS = 5e3;
|
|
1773
|
+
var McpRootsPending = class extends Error {
|
|
1774
|
+
constructor() {
|
|
1775
|
+
super("MCP Roots must be requested from the client before the project can be bound.");
|
|
1776
|
+
this.name = "McpRootsPending";
|
|
1777
|
+
}
|
|
1778
|
+
};
|
|
1766
1779
|
var ProjectBindingError = class extends Error {
|
|
1767
1780
|
diagnostics;
|
|
1768
1781
|
constructor(diagnostics) {
|
|
@@ -1772,18 +1785,19 @@ var ProjectBindingError = class extends Error {
|
|
|
1772
1785
|
}
|
|
1773
1786
|
};
|
|
1774
1787
|
var ProjectBindingResolver = class {
|
|
1775
|
-
constructor(processCwd, rootsProvider, rootsTimeoutMs = MCP_ROOTS_TIMEOUT_MS) {
|
|
1788
|
+
constructor(processCwd, rootsProvider, rootsTimeoutMs = MCP_ROOTS_TIMEOUT_MS, configuredRoot) {
|
|
1776
1789
|
this.processCwd = processCwd;
|
|
1777
1790
|
this.rootsProvider = rootsProvider;
|
|
1778
1791
|
this.rootsTimeoutMs = rootsTimeoutMs;
|
|
1792
|
+
this.configuredRoot = configuredRoot;
|
|
1779
1793
|
}
|
|
1780
1794
|
bound;
|
|
1781
1795
|
boundState;
|
|
1782
1796
|
resolving;
|
|
1783
|
-
async resolve() {
|
|
1797
|
+
async resolve(call) {
|
|
1784
1798
|
if (this.bound) return this.bound;
|
|
1785
1799
|
if (this.resolving) return this.resolving;
|
|
1786
|
-
this.resolving = this.inspect().then((inspection) => {
|
|
1800
|
+
this.resolving = this.inspect(false, call).then((inspection) => {
|
|
1787
1801
|
if (!inspection.selected) throw new ProjectBindingError(inspection.diagnostics);
|
|
1788
1802
|
this.bound = inspection.selected;
|
|
1789
1803
|
this.boundState = inspection.diagnostics;
|
|
@@ -1793,18 +1807,18 @@ var ProjectBindingResolver = class {
|
|
|
1793
1807
|
});
|
|
1794
1808
|
return this.resolving;
|
|
1795
1809
|
}
|
|
1796
|
-
async diagnose() {
|
|
1810
|
+
async diagnose(call) {
|
|
1797
1811
|
if (this.bound) return this.boundState ?? boundDiagnostics(this.processCwd, this.bound);
|
|
1798
|
-
const inspection = await this.inspect();
|
|
1812
|
+
const inspection = await this.inspect(false, call);
|
|
1799
1813
|
if (inspection.selected) {
|
|
1800
1814
|
this.bound = inspection.selected;
|
|
1801
1815
|
this.boundState = inspection.diagnostics;
|
|
1802
1816
|
}
|
|
1803
1817
|
return inspection.diagnostics;
|
|
1804
1818
|
}
|
|
1805
|
-
async initialize() {
|
|
1819
|
+
async initialize(call) {
|
|
1806
1820
|
if (this.bound) return this.bound;
|
|
1807
|
-
const inspection = await this.inspect(true);
|
|
1821
|
+
const inspection = await this.inspect(true, call);
|
|
1808
1822
|
if (inspection.selected) {
|
|
1809
1823
|
this.bound = inspection.selected;
|
|
1810
1824
|
this.boundState = inspection.diagnostics;
|
|
@@ -1814,28 +1828,41 @@ var ProjectBindingResolver = class {
|
|
|
1814
1828
|
throw new ProjectBindingError(inspection.diagnostics);
|
|
1815
1829
|
}
|
|
1816
1830
|
const initialized = initializeProject(inspection.initializableRoot);
|
|
1817
|
-
this.bound = {
|
|
1831
|
+
this.bound = {
|
|
1832
|
+
...initialized,
|
|
1833
|
+
bindingSource: inspection.initializableSource ?? "mcp_root"
|
|
1834
|
+
};
|
|
1818
1835
|
this.boundState = boundDiagnostics(
|
|
1819
1836
|
this.processCwd,
|
|
1820
1837
|
this.bound,
|
|
1821
|
-
{ supported:
|
|
1822
|
-
inspection.diagnostics.rootCandidates
|
|
1838
|
+
{ supported: inspection.diagnostics.mcpRootsSupported, roots: [] },
|
|
1839
|
+
inspection.diagnostics.rootCandidates,
|
|
1840
|
+
inspection.diagnostics.configuredProjectRoot
|
|
1823
1841
|
);
|
|
1824
1842
|
return this.bound;
|
|
1825
1843
|
}
|
|
1826
|
-
async inspect(forInitialization = false) {
|
|
1827
|
-
const snapshot = await safeRootsSnapshot(this.rootsProvider, this.rootsTimeoutMs);
|
|
1844
|
+
async inspect(forInitialization = false, call) {
|
|
1845
|
+
const snapshot = await safeRootsSnapshot(this.rootsProvider, this.rootsTimeoutMs, call);
|
|
1828
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
|
+
});
|
|
1829
1859
|
const initializedRoots = rootCandidates.filter(
|
|
1830
1860
|
(candidate) => candidate.initialized && candidate.path !== void 0
|
|
1831
1861
|
);
|
|
1832
1862
|
if (snapshot.supported && snapshot.error) {
|
|
1833
1863
|
return {
|
|
1834
|
-
diagnostics:
|
|
1864
|
+
diagnostics: diagnose(
|
|
1835
1865
|
"roots_request_failed",
|
|
1836
|
-
snapshot,
|
|
1837
|
-
this.processCwd,
|
|
1838
|
-
rootCandidates,
|
|
1839
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."
|
|
1840
1867
|
)
|
|
1841
1868
|
};
|
|
@@ -1844,19 +1871,12 @@ var ProjectBindingResolver = class {
|
|
|
1844
1871
|
const initializedRoot = initializedRoots[0];
|
|
1845
1872
|
if (!initializedRoot) throw new Error("initialized Root disappeared during resolution");
|
|
1846
1873
|
const project = resolveLockedProjectRoot(initializedRoot.path);
|
|
1847
|
-
|
|
1848
|
-
return {
|
|
1849
|
-
selected,
|
|
1850
|
-
diagnostics: boundDiagnostics(this.processCwd, selected, snapshot, rootCandidates)
|
|
1851
|
-
};
|
|
1874
|
+
return bound({ ...project, bindingSource: "mcp_root" });
|
|
1852
1875
|
}
|
|
1853
1876
|
if (initializedRoots.length > 1) {
|
|
1854
1877
|
return {
|
|
1855
|
-
diagnostics:
|
|
1878
|
+
diagnostics: diagnose(
|
|
1856
1879
|
"multiple_initialized_roots",
|
|
1857
|
-
snapshot,
|
|
1858
|
-
this.processCwd,
|
|
1859
|
-
rootCandidates,
|
|
1860
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."
|
|
1861
1881
|
)
|
|
1862
1882
|
};
|
|
@@ -1870,22 +1890,17 @@ var ProjectBindingResolver = class {
|
|
|
1870
1890
|
if (!validRoot) throw new Error("workspace Root disappeared during initialization");
|
|
1871
1891
|
return {
|
|
1872
1892
|
initializableRoot: validRoot.path,
|
|
1873
|
-
|
|
1893
|
+
initializableSource: "mcp_root",
|
|
1894
|
+
diagnostics: diagnose(
|
|
1874
1895
|
"workspace_not_initialized",
|
|
1875
|
-
snapshot,
|
|
1876
|
-
this.processCwd,
|
|
1877
|
-
rootCandidates,
|
|
1878
1896
|
`The active MCP workspace ${validRoot.path} is ready to initialize.`
|
|
1879
1897
|
)
|
|
1880
1898
|
};
|
|
1881
1899
|
}
|
|
1882
1900
|
if (validRoots.length > 1) {
|
|
1883
1901
|
return {
|
|
1884
|
-
diagnostics:
|
|
1902
|
+
diagnostics: diagnose(
|
|
1885
1903
|
"multiple_uninitialized_roots",
|
|
1886
|
-
snapshot,
|
|
1887
|
-
this.processCwd,
|
|
1888
|
-
rootCandidates,
|
|
1889
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."
|
|
1890
1905
|
)
|
|
1891
1906
|
};
|
|
@@ -1895,54 +1910,59 @@ var ProjectBindingResolver = class {
|
|
|
1895
1910
|
const validRoot = validRoots[0];
|
|
1896
1911
|
if (!validRoot) throw new Error("workspace Root disappeared during diagnosis");
|
|
1897
1912
|
return {
|
|
1898
|
-
diagnostics:
|
|
1913
|
+
diagnostics: diagnose(
|
|
1899
1914
|
"workspace_not_initialized",
|
|
1900
|
-
snapshot,
|
|
1901
|
-
this.processCwd,
|
|
1902
|
-
rootCandidates,
|
|
1903
1915
|
`The IDE workspace ${validRoot.path} is not initialized. Call init with no path arguments; it will create .sakupa directly in that workspace Root.`
|
|
1904
1916
|
)
|
|
1905
1917
|
};
|
|
1906
1918
|
}
|
|
1907
1919
|
if (snapshot.supported && validRoots.length > 1) {
|
|
1908
1920
|
return {
|
|
1909
|
-
diagnostics:
|
|
1921
|
+
diagnostics: diagnose(
|
|
1910
1922
|
"multiple_uninitialized_roots",
|
|
1911
|
-
snapshot,
|
|
1912
|
-
this.processCwd,
|
|
1913
|
-
rootCandidates,
|
|
1914
1923
|
"The IDE exposes multiple uninitialized workspace Roots. Open only the intended project, then call init. Sakupa will not guess a project directory."
|
|
1915
1924
|
)
|
|
1916
1925
|
};
|
|
1917
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
|
+
}
|
|
1918
1948
|
if (snapshot.supported) {
|
|
1919
1949
|
return {
|
|
1920
|
-
diagnostics:
|
|
1950
|
+
diagnostics: diagnose(
|
|
1921
1951
|
"workspace_not_initialized",
|
|
1922
|
-
snapshot,
|
|
1923
|
-
this.processCwd,
|
|
1924
|
-
rootCandidates,
|
|
1925
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."
|
|
1926
1953
|
)
|
|
1927
1954
|
};
|
|
1928
1955
|
}
|
|
1929
1956
|
try {
|
|
1930
1957
|
const cwdProject = resolveLockedProjectRoot(this.processCwd);
|
|
1931
|
-
|
|
1932
|
-
return {
|
|
1933
|
-
selected,
|
|
1934
|
-
diagnostics: boundDiagnostics(this.processCwd, selected, snapshot, rootCandidates)
|
|
1935
|
-
};
|
|
1958
|
+
return bound({ ...cwdProject, bindingSource: "process_cwd" });
|
|
1936
1959
|
} catch {
|
|
1937
1960
|
}
|
|
1938
1961
|
const cwdProblem = inspectDirectory(this.processCwd);
|
|
1939
1962
|
return {
|
|
1940
|
-
diagnostics:
|
|
1963
|
+
diagnostics: diagnose(
|
|
1941
1964
|
cwdProblem.problem ? "invalid_process_cwd" : "process_cwd_is_not_workspace",
|
|
1942
|
-
|
|
1943
|
-
this.processCwd,
|
|
1944
|
-
rootCandidates,
|
|
1945
|
-
"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."
|
|
1946
1966
|
)
|
|
1947
1967
|
};
|
|
1948
1968
|
}
|
|
@@ -1952,11 +1972,12 @@ function fileRootUriToPath(uri, windows = process.platform === "win32") {
|
|
|
1952
1972
|
if (parsed.protocol !== "file:") throw new Error("Root URI is not a file URI");
|
|
1953
1973
|
return fileURLToPath(parsed, { windows });
|
|
1954
1974
|
}
|
|
1955
|
-
async function safeRootsSnapshot(provider, timeoutMs = MCP_ROOTS_TIMEOUT_MS) {
|
|
1975
|
+
async function safeRootsSnapshot(provider, timeoutMs = MCP_ROOTS_TIMEOUT_MS, call) {
|
|
1956
1976
|
if (!provider) return { supported: false, roots: [] };
|
|
1957
1977
|
try {
|
|
1958
|
-
return await withOperationTimeout("MCP Roots request", timeoutMs, () => provider());
|
|
1978
|
+
return await withOperationTimeout("MCP Roots request", timeoutMs, () => provider(call));
|
|
1959
1979
|
} catch (error) {
|
|
1980
|
+
if (error instanceof McpRootsPending) throw error;
|
|
1960
1981
|
return {
|
|
1961
1982
|
supported: true,
|
|
1962
1983
|
roots: [],
|
|
@@ -1984,6 +2005,31 @@ function inspectRoot(root) {
|
|
|
1984
2005
|
};
|
|
1985
2006
|
}
|
|
1986
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
|
+
}
|
|
1987
2033
|
function inspectDirectory(path) {
|
|
1988
2034
|
try {
|
|
1989
2035
|
return { path: canonicalProjectDirectory(resolve3(path)) };
|
|
@@ -1991,22 +2037,24 @@ function inspectDirectory(path) {
|
|
|
1991
2037
|
return { problem: error instanceof Error ? error.message : String(error) };
|
|
1992
2038
|
}
|
|
1993
2039
|
}
|
|
1994
|
-
function diagnostic(diagnosisCode, snapshot, processCwd, rootCandidates, guidance) {
|
|
2040
|
+
function diagnostic(diagnosisCode, snapshot, processCwd, rootCandidates, guidance, configured) {
|
|
1995
2041
|
return {
|
|
1996
2042
|
diagnosisCode,
|
|
1997
2043
|
mcpRootsSupported: snapshot.supported,
|
|
1998
2044
|
processCwd,
|
|
1999
2045
|
rootCandidates,
|
|
2046
|
+
...configured ? { configuredProjectRoot: configured } : {},
|
|
2000
2047
|
guidance,
|
|
2001
2048
|
reportRecommended: false
|
|
2002
2049
|
};
|
|
2003
2050
|
}
|
|
2004
|
-
function boundDiagnostics(processCwd, selected, snapshot = { supported: false, roots: [] }, rootCandidates = []) {
|
|
2051
|
+
function boundDiagnostics(processCwd, selected, snapshot = { supported: false, roots: [] }, rootCandidates = [], configured) {
|
|
2005
2052
|
return {
|
|
2006
2053
|
diagnosisCode: "project_bound",
|
|
2007
2054
|
mcpRootsSupported: snapshot.supported,
|
|
2008
2055
|
processCwd,
|
|
2009
2056
|
rootCandidates,
|
|
2057
|
+
...configured ? { configuredProjectRoot: configured } : {},
|
|
2010
2058
|
selectedProjectDir: selected.projectDir,
|
|
2011
2059
|
bindingSource: selected.bindingSource,
|
|
2012
2060
|
guidance: `Sakupa is locked to ${selected.projectDir} from ${selected.bindingSource}.`,
|
|
@@ -2034,7 +2082,7 @@ var TARGET_MCP_TOOL_NAMES = [
|
|
|
2034
2082
|
"support",
|
|
2035
2083
|
"report"
|
|
2036
2084
|
];
|
|
2037
|
-
var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
|
|
2085
|
+
var STRUCTURED_TOOL_OUTPUT_SCHEMA = z.object({
|
|
2038
2086
|
schemaVersion: z.literal(1),
|
|
2039
2087
|
outcome: z.enum([
|
|
2040
2088
|
"completed",
|
|
@@ -2116,7 +2164,7 @@ var STRUCTURED_TOOL_OUTPUT_SCHEMA = {
|
|
|
2116
2164
|
reasonCode: z.string().optional()
|
|
2117
2165
|
})
|
|
2118
2166
|
)
|
|
2119
|
-
};
|
|
2167
|
+
});
|
|
2120
2168
|
function structuredToolResult(envelope) {
|
|
2121
2169
|
const clientTimeZone = clientRuntimeTimeZone();
|
|
2122
2170
|
const presentation = {
|
|
@@ -2225,14 +2273,19 @@ function resolverFor(ctx) {
|
|
|
2225
2273
|
if (ctx.projectBinding) return ctx.projectBinding;
|
|
2226
2274
|
let resolver = fallbackResolvers.get(ctx);
|
|
2227
2275
|
if (!resolver) {
|
|
2228
|
-
resolver = new ProjectBindingResolver(
|
|
2276
|
+
resolver = new ProjectBindingResolver(
|
|
2277
|
+
ctx.projectDir,
|
|
2278
|
+
ctx.rootsProvider,
|
|
2279
|
+
MCP_ROOTS_TIMEOUT_MS,
|
|
2280
|
+
ctx.configuredProjectRoot
|
|
2281
|
+
);
|
|
2229
2282
|
fallbackResolvers.set(ctx, resolver);
|
|
2230
2283
|
}
|
|
2231
2284
|
return resolver;
|
|
2232
2285
|
}
|
|
2233
|
-
async function withProjectDir(ctx) {
|
|
2286
|
+
async function withProjectDir(ctx, call) {
|
|
2234
2287
|
try {
|
|
2235
|
-
const binding = await resolverFor(ctx).resolve();
|
|
2288
|
+
const binding = await resolverFor(ctx).resolve(call);
|
|
2236
2289
|
const resolved = resolveLockedProjectRoot(binding.projectDir);
|
|
2237
2290
|
return {
|
|
2238
2291
|
...ctx,
|
|
@@ -2255,12 +2308,12 @@ async function withProjectDir(ctx) {
|
|
|
2255
2308
|
throw error;
|
|
2256
2309
|
}
|
|
2257
2310
|
}
|
|
2258
|
-
async function diagnoseProjectBinding(ctx) {
|
|
2259
|
-
return resolverFor(ctx).diagnose();
|
|
2311
|
+
async function diagnoseProjectBinding(ctx, call) {
|
|
2312
|
+
return resolverFor(ctx).diagnose(call);
|
|
2260
2313
|
}
|
|
2261
|
-
async function initializeWorkspaceProject(ctx) {
|
|
2314
|
+
async function initializeWorkspaceProject(ctx, call) {
|
|
2262
2315
|
try {
|
|
2263
|
-
const resolved = await resolverFor(ctx).initialize();
|
|
2316
|
+
const resolved = await resolverFor(ctx).initialize(call);
|
|
2264
2317
|
return {
|
|
2265
2318
|
...ctx,
|
|
2266
2319
|
projectDir: resolved.projectDir,
|
|
@@ -2276,10 +2329,11 @@ async function initializeWorkspaceProject(ctx) {
|
|
|
2276
2329
|
throw error;
|
|
2277
2330
|
}
|
|
2278
2331
|
}
|
|
2279
|
-
async function optionalProjectContext(ctx) {
|
|
2332
|
+
async function optionalProjectContext(ctx, call) {
|
|
2280
2333
|
try {
|
|
2281
|
-
return await withProjectDir(ctx);
|
|
2282
|
-
} catch {
|
|
2334
|
+
return await withProjectDir(ctx, call);
|
|
2335
|
+
} catch (error) {
|
|
2336
|
+
if (error instanceof McpRootsPending) throw error;
|
|
2283
2337
|
return null;
|
|
2284
2338
|
}
|
|
2285
2339
|
}
|
|
@@ -2327,6 +2381,9 @@ function requireSiteFile(ctx) {
|
|
|
2327
2381
|
}
|
|
2328
2382
|
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
2383
|
function toolError(e) {
|
|
2384
|
+
if (e instanceof McpRootsPending) {
|
|
2385
|
+
return inputRequired({ inputRequests: { roots: inputRequired.listRoots() } });
|
|
2386
|
+
}
|
|
2330
2387
|
const isSakupa = isSakupaError(e);
|
|
2331
2388
|
const errorCode = isSakupa ? e.code : "internal";
|
|
2332
2389
|
const rawDetails = isSakupaError(e) && e.details && typeof e.details === "object" ? e.details : void 0;
|
|
@@ -2398,7 +2455,7 @@ import { z as z2 } from "zod";
|
|
|
2398
2455
|
// src/recovery-archive.ts
|
|
2399
2456
|
import { existsSync as existsSync3, realpathSync as realpathSync2 } from "node:fs";
|
|
2400
2457
|
import { mkdtemp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2401
|
-
import { dirname as dirname2, isAbsolute as
|
|
2458
|
+
import { dirname as dirname2, isAbsolute as isAbsolute3, join as join4, relative as relative2, resolve as resolve4, sep as sep3 } from "node:path";
|
|
2402
2459
|
|
|
2403
2460
|
// ../../node_modules/fflate/esm/index.mjs
|
|
2404
2461
|
import { createRequire } from "module";
|
|
@@ -2885,13 +2942,13 @@ function unzipSync(data, opts) {
|
|
|
2885
2942
|
|
|
2886
2943
|
// src/recovery-archive.ts
|
|
2887
2944
|
function safeOutputPath(projectDir, outputDir) {
|
|
2888
|
-
if (outputDir.length === 0 ||
|
|
2945
|
+
if (outputDir.length === 0 || isAbsolute3(outputDir)) {
|
|
2889
2946
|
throw new SakupaError("invalid_request", "Recovery outputDir must be a relative directory");
|
|
2890
2947
|
}
|
|
2891
2948
|
const root = realpathSync2(resolve4(projectDir));
|
|
2892
2949
|
const target = resolve4(root, outputDir);
|
|
2893
2950
|
const rel = relative2(root, target);
|
|
2894
|
-
if (rel === "" || rel === ".." || rel.startsWith(`..${sep3}`) ||
|
|
2951
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${sep3}`) || isAbsolute3(rel)) {
|
|
2895
2952
|
throw new SakupaError("invalid_request", "Recovery outputDir must stay inside projectDir");
|
|
2896
2953
|
}
|
|
2897
2954
|
if (rel === ".sakupa" || rel.startsWith(`.sakupa${sep3}`)) {
|
|
@@ -2906,7 +2963,7 @@ function safeOutputPath(projectDir, outputDir) {
|
|
|
2906
2963
|
const physicalAncestor = realpathSync2(existingAncestor);
|
|
2907
2964
|
const physicalTarget = resolve4(physicalAncestor, relative2(existingAncestor, target));
|
|
2908
2965
|
const physicalRel = relative2(root, physicalTarget);
|
|
2909
|
-
if (physicalRel === ".." || physicalRel.startsWith(`..${sep3}`) ||
|
|
2966
|
+
if (physicalRel === ".." || physicalRel.startsWith(`..${sep3}`) || isAbsolute3(physicalRel)) {
|
|
2910
2967
|
throw new SakupaError(
|
|
2911
2968
|
"invalid_request",
|
|
2912
2969
|
"Recovery outputDir resolves through a symlink outside projectDir"
|
|
@@ -3272,7 +3329,7 @@ import {
|
|
|
3272
3329
|
writeFileSync as writeFileSync5
|
|
3273
3330
|
} from "node:fs";
|
|
3274
3331
|
import { createHash } from "node:crypto";
|
|
3275
|
-
import { dirname as dirname5, isAbsolute as
|
|
3332
|
+
import { dirname as dirname5, isAbsolute as isAbsolute4, join as join7 } from "node:path";
|
|
3276
3333
|
var HANDOFF_LOCK_TTL_MS = 15 * 60 * 1e3;
|
|
3277
3334
|
function normalizeSiteUrl(raw) {
|
|
3278
3335
|
const url = new URL(raw);
|
|
@@ -3299,7 +3356,7 @@ function resolveReusableSite(rawUrl, currentProjectDir, nowMs, apiBaseUrl) {
|
|
|
3299
3356
|
throw new Error(`More than one local free-site record matches ${siteUrl}.`);
|
|
3300
3357
|
const record = matches2[0];
|
|
3301
3358
|
if (!record) throw new Error("The selected existing free site disappeared during resolution.");
|
|
3302
|
-
if (!
|
|
3359
|
+
if (!isAbsolute4(record.projectDir)) {
|
|
3303
3360
|
throw new Error("The selected free-site project path is not absolute; refusing cwd lookup.");
|
|
3304
3361
|
}
|
|
3305
3362
|
const sourceProjectDir = canonicalProjectDirectory(record.projectDir);
|
|
@@ -4146,13 +4203,13 @@ function registerTools(server, baseCtx) {
|
|
|
4146
4203
|
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
4204
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4148
4205
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
4149
|
-
inputSchema: {
|
|
4206
|
+
inputSchema: z2.object({
|
|
4150
4207
|
outputDir: z2.string().optional().describe("Output directory relative to the project root (overrides detection).")
|
|
4151
|
-
}
|
|
4208
|
+
})
|
|
4152
4209
|
},
|
|
4153
|
-
async (args) => {
|
|
4210
|
+
async (args, call) => {
|
|
4154
4211
|
try {
|
|
4155
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4212
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4156
4213
|
const analysis = await analyzeProject(ctx.projectDir, {
|
|
4157
4214
|
...args.outputDir !== void 0 ? { outputDir: args.outputDir } : {}
|
|
4158
4215
|
});
|
|
@@ -4173,7 +4230,7 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
4173
4230
|
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
4231
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4175
4232
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
4176
|
-
inputSchema: {
|
|
4233
|
+
inputSchema: z2.object({
|
|
4177
4234
|
outputDir: z2.string().min(1).describe(
|
|
4178
4235
|
'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
4236
|
),
|
|
@@ -4199,12 +4256,12 @@ Next action: ${analysis.suggestedNextAction}`,
|
|
|
4199
4256
|
"Deprecated compatibility field. Project independence is established only by `sakupa-mcp init`, never inferred from package.json or folder names."
|
|
4200
4257
|
),
|
|
4201
4258
|
lang: z2.string().optional().describe("Site language override (en | ja | zh-CN); defaults to the html lang.")
|
|
4202
|
-
}
|
|
4259
|
+
})
|
|
4203
4260
|
},
|
|
4204
|
-
async (args) => {
|
|
4261
|
+
async (args, call) => {
|
|
4205
4262
|
let releaseHandoffLock;
|
|
4206
4263
|
try {
|
|
4207
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4264
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4208
4265
|
const analysis = await analyzeProject(ctx.projectDir, { outputDir: args.outputDir });
|
|
4209
4266
|
if (!analysis.deployable || !analysis.files) {
|
|
4210
4267
|
return notDeployableResult(analysis);
|
|
@@ -4829,11 +4886,11 @@ Optional security recommendation: this management credential was created at ${ti
|
|
|
4829
4886
|
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
4887
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4831
4888
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
4832
|
-
inputSchema: {}
|
|
4889
|
+
inputSchema: z2.object({})
|
|
4833
4890
|
},
|
|
4834
|
-
async () => {
|
|
4891
|
+
async (_args, call) => {
|
|
4835
4892
|
try {
|
|
4836
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4893
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4837
4894
|
const site = requireSiteFile(ctx);
|
|
4838
4895
|
const res = await ctx.client.refreshSite(site.siteId, site.credential);
|
|
4839
4896
|
if (site.url) {
|
|
@@ -4862,11 +4919,11 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
4862
4919
|
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
4920
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4864
4921
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
4865
|
-
inputSchema: {}
|
|
4922
|
+
inputSchema: z2.object({})
|
|
4866
4923
|
},
|
|
4867
|
-
async () => {
|
|
4924
|
+
async (_args, call) => {
|
|
4868
4925
|
try {
|
|
4869
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4926
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4870
4927
|
const site = requireSiteFile(ctx);
|
|
4871
4928
|
const res = await ctx.client.getSiteStatus(site.siteId, site.credential);
|
|
4872
4929
|
noteSiteMode(res.siteId, res.mode);
|
|
@@ -4893,15 +4950,15 @@ NO content was uploaded or changed by this call \u2014 to publish new or edited
|
|
|
4893
4950
|
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
4951
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4895
4952
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
4896
|
-
inputSchema: {
|
|
4953
|
+
inputSchema: z2.object({
|
|
4897
4954
|
plan: planEnum.describe(
|
|
4898
4955
|
"Monthly plan: water (very light personal pages), personal (personal brand / small shop), share (small-business site), business (steadier traffic, more headroom)."
|
|
4899
4956
|
)
|
|
4900
|
-
}
|
|
4957
|
+
})
|
|
4901
4958
|
},
|
|
4902
|
-
async (args) => {
|
|
4959
|
+
async (args, call) => {
|
|
4903
4960
|
try {
|
|
4904
|
-
const ctx = await withProjectDir(baseCtx);
|
|
4961
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4905
4962
|
const site = requireSiteFile(ctx);
|
|
4906
4963
|
const res = await ctx.client.createPlanCheckout(
|
|
4907
4964
|
{
|
|
@@ -4939,17 +4996,17 @@ Once Stripe confirms payment and Sakupa synchronizes the subscription, the curre
|
|
|
4939
4996
|
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
4997
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
4941
4998
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
4942
|
-
inputSchema: {
|
|
4999
|
+
inputSchema: z2.object({
|
|
4943
5000
|
action: z2.enum(["start", "status"]),
|
|
4944
5001
|
hostname: z2.string().optional().describe("Required for start."),
|
|
4945
5002
|
verificationId: z2.string().optional().describe(
|
|
4946
5003
|
"Optional for status: when omitted, the server finds this site's latest binding verification \u2014 a NEW session can resume without it."
|
|
4947
5004
|
)
|
|
4948
|
-
}
|
|
5005
|
+
})
|
|
4949
5006
|
},
|
|
4950
|
-
async (args) => {
|
|
5007
|
+
async (args, call) => {
|
|
4951
5008
|
try {
|
|
4952
|
-
const ctx = await withProjectDir(baseCtx);
|
|
5009
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
4953
5010
|
const site = requireSiteFile(ctx);
|
|
4954
5011
|
if (args.action === "status") {
|
|
4955
5012
|
const res2 = args.verificationId ? await ctx.client.checkVerification(args.verificationId, site.credential) : await ctx.client.checkVerification("latest", site.credential, site.siteId);
|
|
@@ -5078,11 +5135,11 @@ When the user says the TXT is set, run bind "status". It verifies ownership and
|
|
|
5078
5135
|
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
5136
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5080
5137
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
5081
|
-
inputSchema: {}
|
|
5138
|
+
inputSchema: z2.object({})
|
|
5082
5139
|
},
|
|
5083
|
-
async () => {
|
|
5140
|
+
async (_args, call) => {
|
|
5084
5141
|
try {
|
|
5085
|
-
const ctx = await withProjectDir(baseCtx);
|
|
5142
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
5086
5143
|
const site = requireSiteFile(ctx);
|
|
5087
5144
|
const res = await ctx.client.getBillingStatus(site.siteId, site.credential);
|
|
5088
5145
|
noteSiteMode(res.siteId, res.mode);
|
|
@@ -5122,14 +5179,14 @@ Full status:`, res);
|
|
|
5122
5179
|
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
5180
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5124
5181
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
5125
|
-
inputSchema: {
|
|
5182
|
+
inputSchema: z2.object({
|
|
5126
5183
|
scope: z2.enum(["site", "public_recovery"])
|
|
5127
|
-
}
|
|
5184
|
+
})
|
|
5128
5185
|
},
|
|
5129
|
-
async (args) => {
|
|
5186
|
+
async (args, call) => {
|
|
5130
5187
|
try {
|
|
5131
5188
|
if (args.scope === "site") {
|
|
5132
|
-
const ctx = await withProjectDir(baseCtx);
|
|
5189
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
5133
5190
|
const site = requireSiteFile(ctx);
|
|
5134
5191
|
const res2 = await ctx.client.createBillingPortal(site.siteId, site.credential);
|
|
5135
5192
|
return structuredToolResult({
|
|
@@ -5178,7 +5235,7 @@ Full status:`, res);
|
|
|
5178
5235
|
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
5236
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5180
5237
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
5181
|
-
inputSchema: {
|
|
5238
|
+
inputSchema: z2.object({
|
|
5182
5239
|
action: z2.enum(["start", "status", "complete", "download"]),
|
|
5183
5240
|
hostname: z2.string().optional().describe("Required for start."),
|
|
5184
5241
|
verificationId: z2.string().optional().describe("For status or complete; inferred from local recovery state when omitted."),
|
|
@@ -5186,11 +5243,11 @@ Full status:`, res);
|
|
|
5186
5243
|
"REQUIRED for complete/download: exact extraction directory relative to the initialized project root. Inspect the current project; Sakupa never guesses a name."
|
|
5187
5244
|
),
|
|
5188
5245
|
preserveExistingCredentials: z2.boolean().optional().describe("Explicitly keep old local credentials working (default: revoke them all).")
|
|
5189
|
-
}
|
|
5246
|
+
})
|
|
5190
5247
|
},
|
|
5191
|
-
async (args) => {
|
|
5248
|
+
async (args, call) => {
|
|
5192
5249
|
try {
|
|
5193
|
-
const ctx = await withProjectDir(baseCtx);
|
|
5250
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
5194
5251
|
if ((args.action === "complete" || args.action === "download") && args.outputDir === void 0) {
|
|
5195
5252
|
throw new LocalGuidanceError(
|
|
5196
5253
|
"invalid_request",
|
|
@@ -5539,16 +5596,16 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
5539
5596
|
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
5597
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5541
5598
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
5542
|
-
inputSchema: {
|
|
5599
|
+
inputSchema: z2.object({
|
|
5543
5600
|
category: ticketCategoryEnum,
|
|
5544
5601
|
subject: z2.string().describe("Short subject line."),
|
|
5545
5602
|
description: z2.string().describe("Problem description (no secrets, no card data)."),
|
|
5546
5603
|
contactEmail: z2.string().optional().describe("Optional contact email for follow-up.")
|
|
5547
|
-
}
|
|
5604
|
+
})
|
|
5548
5605
|
},
|
|
5549
|
-
async (args) => {
|
|
5606
|
+
async (args, call) => {
|
|
5550
5607
|
try {
|
|
5551
|
-
const ctx = await withProjectDir(baseCtx);
|
|
5608
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
5552
5609
|
const site = requireSiteFile(ctx);
|
|
5553
5610
|
const res = await ctx.client.createTicket(site.credential, {
|
|
5554
5611
|
siteId: site.siteId,
|
|
@@ -5573,7 +5630,7 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
5573
5630
|
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
5631
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5575
5632
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
5576
|
-
inputSchema: {
|
|
5633
|
+
inputSchema: z2.object({
|
|
5577
5634
|
toolName: z2.string().describe('The Sakupa tool that failed, e.g. "deploy".'),
|
|
5578
5635
|
helpAuthorization: z2.string().describe("Short-lived authorization returned only by help when report is recommended."),
|
|
5579
5636
|
errorCode: z2.string().optional(),
|
|
@@ -5589,12 +5646,12 @@ Files: ${extracted.fileCount}; bytes: ${extracted.totalBytes}
|
|
|
5589
5646
|
"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
5647
|
),
|
|
5591
5648
|
confirmSubmit: z2.boolean().optional().describe("User reviewed the report payload and approved submission.")
|
|
5592
|
-
}
|
|
5649
|
+
})
|
|
5593
5650
|
},
|
|
5594
|
-
async (args) => {
|
|
5651
|
+
async (args, call) => {
|
|
5595
5652
|
try {
|
|
5596
5653
|
requireReportAuthorization(baseCtx, args.helpAuthorization, args.toolName);
|
|
5597
|
-
const ctx = await optionalProjectContext(baseCtx);
|
|
5654
|
+
const ctx = await optionalProjectContext(baseCtx, call);
|
|
5598
5655
|
const siteState = ctx ? loadSiteFile(ctx.projectDir) : { kind: "absent" };
|
|
5599
5656
|
const site = siteState.kind === "ok" ? siteState.file : null;
|
|
5600
5657
|
const diagnostics = {
|
|
@@ -5668,7 +5725,11 @@ Summary: ${res.sanitizedSummary}`,
|
|
|
5668
5725
|
}
|
|
5669
5726
|
|
|
5670
5727
|
// src/server.ts
|
|
5671
|
-
import {
|
|
5728
|
+
import {
|
|
5729
|
+
CLIENT_CAPABILITIES_META_KEY,
|
|
5730
|
+
McpServer,
|
|
5731
|
+
inputResponse
|
|
5732
|
+
} from "@modelcontextprotocol/server";
|
|
5672
5733
|
|
|
5673
5734
|
// src/tools/billing.ts
|
|
5674
5735
|
import { z as z3 } from "zod";
|
|
@@ -5677,11 +5738,11 @@ function registerBillingTools(server, baseCtx) {
|
|
|
5677
5738
|
"plans",
|
|
5678
5739
|
{
|
|
5679
5740
|
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: {},
|
|
5741
|
+
inputSchema: z3.object({}),
|
|
5681
5742
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5682
5743
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true }
|
|
5683
5744
|
},
|
|
5684
|
-
async () => {
|
|
5745
|
+
async (_args, call) => {
|
|
5685
5746
|
try {
|
|
5686
5747
|
const catalog = await baseCtx.client.getBillingPlanCatalog();
|
|
5687
5748
|
return structuredToolResult({
|
|
@@ -5701,15 +5762,15 @@ function registerBillingTools(server, baseCtx) {
|
|
|
5701
5762
|
"change",
|
|
5702
5763
|
{
|
|
5703
5764
|
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: {
|
|
5765
|
+
inputSchema: z3.object({
|
|
5705
5766
|
operationId: z3.string().min(1)
|
|
5706
|
-
},
|
|
5767
|
+
}),
|
|
5707
5768
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5708
5769
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true }
|
|
5709
5770
|
},
|
|
5710
|
-
async (args) => {
|
|
5771
|
+
async (args, call) => {
|
|
5711
5772
|
try {
|
|
5712
|
-
const ctx = await withProjectDir(baseCtx);
|
|
5773
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
5713
5774
|
const site = requireSiteFile(ctx);
|
|
5714
5775
|
const result = await ctx.client.changeSubscriptionPlan(site.credential, {
|
|
5715
5776
|
siteId: site.siteId,
|
|
@@ -5793,7 +5854,7 @@ var TOOL_MANUALS = {
|
|
|
5793
5854
|
init: {
|
|
5794
5855
|
purpose: "Initialize the active IDE workspace as one Sakupa project.",
|
|
5795
5856
|
sideEffects: "Creates only .sakupa/project.json locally; no API call, site or charge.",
|
|
5796
|
-
preconditions: "Exactly one usable MCP workspace Root. If
|
|
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.",
|
|
5797
5858
|
parameterNames: [],
|
|
5798
5859
|
parameters: "No parameters and no path argument.",
|
|
5799
5860
|
warnings: [
|
|
@@ -5992,13 +6053,13 @@ function registerHelpTools(server, baseCtx) {
|
|
|
5992
6053
|
"init",
|
|
5993
6054
|
{
|
|
5994
6055
|
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: {},
|
|
6056
|
+
inputSchema: z4.object({}),
|
|
5996
6057
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
5997
6058
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
5998
6059
|
},
|
|
5999
|
-
async () => {
|
|
6060
|
+
async (_args, call) => {
|
|
6000
6061
|
try {
|
|
6001
|
-
const ctx = await initializeWorkspaceProject(baseCtx);
|
|
6062
|
+
const ctx = await initializeWorkspaceProject(baseCtx, call);
|
|
6002
6063
|
const marker = loadProjectMarker(ctx.projectDir);
|
|
6003
6064
|
if (marker.kind !== "ok")
|
|
6004
6065
|
throw new Error("init postcondition failed: project marker missing");
|
|
@@ -6032,17 +6093,17 @@ function registerHelpTools(server, baseCtx) {
|
|
|
6032
6093
|
"help",
|
|
6033
6094
|
{
|
|
6034
6095
|
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: {
|
|
6096
|
+
inputSchema: z4.object({
|
|
6036
6097
|
topic: z4.enum(HELP_TOPICS).optional().default("diagnose"),
|
|
6037
6098
|
failedTool: z4.string().optional(),
|
|
6038
6099
|
errorCode: z4.string().optional(),
|
|
6039
6100
|
resultCode: z4.string().optional(),
|
|
6040
6101
|
requestId: z4.string().optional()
|
|
6041
|
-
},
|
|
6102
|
+
}),
|
|
6042
6103
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
6043
6104
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false }
|
|
6044
6105
|
},
|
|
6045
|
-
async (args) => {
|
|
6106
|
+
async (args, call) => {
|
|
6046
6107
|
try {
|
|
6047
6108
|
if (args.topic === "overview") {
|
|
6048
6109
|
const catalog = Object.fromEntries(
|
|
@@ -6104,7 +6165,7 @@ Terminology: ${terminologyText}` : ""),
|
|
|
6104
6165
|
nextActions: []
|
|
6105
6166
|
});
|
|
6106
6167
|
}
|
|
6107
|
-
const diagnosis = await diagnoseProjectBinding(baseCtx);
|
|
6168
|
+
const diagnosis = await diagnoseProjectBinding(baseCtx, call);
|
|
6108
6169
|
const selected = diagnosis.selectedProjectDir;
|
|
6109
6170
|
const marker = selected ? loadProjectMarker(selected) : { kind: "absent" };
|
|
6110
6171
|
const site = selected ? loadSiteFile(selected) : { kind: "absent" };
|
|
@@ -6183,18 +6244,18 @@ function registerCredentialTools(server, baseCtx) {
|
|
|
6183
6244
|
"rotate",
|
|
6184
6245
|
{
|
|
6185
6246
|
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: {
|
|
6247
|
+
inputSchema: z5.object({
|
|
6187
6248
|
confirmed: z5.boolean().optional().describe(
|
|
6188
6249
|
"True only after showing the rotate preview and the user explicitly approves revoking every old credential."
|
|
6189
6250
|
)
|
|
6190
|
-
},
|
|
6251
|
+
}),
|
|
6191
6252
|
outputSchema: STRUCTURED_TOOL_OUTPUT_SCHEMA,
|
|
6192
6253
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true }
|
|
6193
6254
|
},
|
|
6194
|
-
async (args) => {
|
|
6255
|
+
async (args, call) => {
|
|
6195
6256
|
let releaseLock;
|
|
6196
6257
|
try {
|
|
6197
|
-
const ctx = await withProjectDir(baseCtx);
|
|
6258
|
+
const ctx = await withProjectDir(baseCtx, call);
|
|
6198
6259
|
let site = requireSiteFile(ctx);
|
|
6199
6260
|
const pending = loadCredentialRotation(ctx.projectDir);
|
|
6200
6261
|
if (pending.kind !== "absent" || args.confirmed === true) {
|
|
@@ -6350,11 +6411,13 @@ language. Keep option IDs, tool names, exact arguments, URLs, field names and co
|
|
|
6350
6411
|
unchanged.
|
|
6351
6412
|
|
|
6352
6413
|
Project directory contract: before the first deploy or a new recovery, CALL the init MCP tool with
|
|
6353
|
-
NO path argument. init uses the IDE's exact MCP Root
|
|
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
|
|
6354
6416
|
.sakupa/project.json directly there. Do not merely print installation or CLI instructions when the
|
|
6355
|
-
init tool is available. Only after help confirms that the client
|
|
6356
|
-
AI itself use the CLI command "npx -y @sakupa/mcp@latest init" as a
|
|
6357
|
-
run it. The CLI also accepts NO path argument. ONE MCP process = ONE
|
|
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.
|
|
6358
6421
|
Site tools do not accept projectDir and cannot select another root; help, plans and report preview
|
|
6359
6422
|
and public_recovery portal remain project-independent.
|
|
6360
6423
|
Sakupa stores .sakupa/site.json and recovery state only in the locked directory; it never uses
|
|
@@ -6434,29 +6497,19 @@ function createSakupaMcpServer(opts) {
|
|
|
6434
6497
|
{ instructions: instructionsFor(previewHostPatternFor(opts.apiBaseUrl)) }
|
|
6435
6498
|
);
|
|
6436
6499
|
const processCwd = resolve6(opts.projectDir ?? process.cwd());
|
|
6437
|
-
const rootsProvider = opts.rootsProvider ?? (
|
|
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
|
-
});
|
|
6500
|
+
const rootsProvider = opts.rootsProvider ?? ((call) => readClientRoots(server, call));
|
|
6454
6501
|
const ctx = {
|
|
6455
6502
|
client,
|
|
6456
6503
|
apiBaseUrl: opts.apiBaseUrl,
|
|
6457
6504
|
projectDir: processCwd,
|
|
6458
6505
|
rootsProvider,
|
|
6459
|
-
|
|
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
|
+
)
|
|
6460
6513
|
};
|
|
6461
6514
|
registerTools(server, ctx);
|
|
6462
6515
|
registerBillingTools(server, ctx);
|
|
@@ -6464,6 +6517,38 @@ function createSakupaMcpServer(opts) {
|
|
|
6464
6517
|
registerHelpTools(server, ctx);
|
|
6465
6518
|
return server;
|
|
6466
6519
|
}
|
|
6520
|
+
async function readClientRoots(server, call) {
|
|
6521
|
+
if (call?.mcpReq.envelope !== void 0) {
|
|
6522
|
+
const envelope = call.mcpReq.envelope;
|
|
6523
|
+
const declared = envelope[CLIENT_CAPABILITIES_META_KEY];
|
|
6524
|
+
if (!declared?.roots) return { supported: false, roots: [] };
|
|
6525
|
+
const answered = inputResponse(call.mcpReq.inputResponses, "roots");
|
|
6526
|
+
if (answered.kind === "roots") return { supported: true, roots: answered.roots };
|
|
6527
|
+
if (call.mcpReq.inputResponses !== void 0) {
|
|
6528
|
+
return {
|
|
6529
|
+
supported: true,
|
|
6530
|
+
roots: [],
|
|
6531
|
+
error: "The client retried without answering the embedded roots/list request."
|
|
6532
|
+
};
|
|
6533
|
+
}
|
|
6534
|
+
throw new McpRootsPending();
|
|
6535
|
+
}
|
|
6536
|
+
const capabilities = server.server.getClientCapabilities();
|
|
6537
|
+
if (!capabilities?.roots) return { supported: false, roots: [] };
|
|
6538
|
+
try {
|
|
6539
|
+
const response = await server.server.listRoots(void 0, {
|
|
6540
|
+
timeout: MCP_ROOTS_TIMEOUT_MS,
|
|
6541
|
+
maxTotalTimeout: MCP_ROOTS_TIMEOUT_MS
|
|
6542
|
+
});
|
|
6543
|
+
return { supported: true, roots: response.roots };
|
|
6544
|
+
} catch (error) {
|
|
6545
|
+
return {
|
|
6546
|
+
supported: true,
|
|
6547
|
+
roots: [],
|
|
6548
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6549
|
+
};
|
|
6550
|
+
}
|
|
6551
|
+
}
|
|
6467
6552
|
export {
|
|
6468
6553
|
CLIENT_TYPE,
|
|
6469
6554
|
FetchTransport,
|