@cloudgrid-io/mcp 0.5.4 → 0.5.5
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/package.json +2 -1
- package/src/tools.js +63 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "MCP server for CloudGrid. Three editions: a local stdio server (full toolset), a hosted web server (light, CLI-free toolset), and a public docs server (search_cloudgrid_documentation) over MCP Streamable HTTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"smoke:redrop": "node test/smoke-redrop.mjs",
|
|
25
25
|
"smoke:oauth": "node test/smoke-oauth.mjs",
|
|
26
26
|
"test:auth": "node test/auth.test.mjs",
|
|
27
|
+
"test:win-cli": "node test/windows-cli-resolution.test.mjs",
|
|
27
28
|
"smoke:docs": "node test/smoke-docs.mjs",
|
|
28
29
|
"start:docs": "node src/docs.js",
|
|
29
30
|
"snapshot:corpus": "node scripts/snapshot-corpus.mjs",
|
package/src/tools.js
CHANGED
|
@@ -13,6 +13,7 @@ import { promisify } from "node:util";
|
|
|
13
13
|
import { readFile } from "node:fs/promises";
|
|
14
14
|
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
15
15
|
import { basename, dirname, resolve, join } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
16
17
|
import { z } from "zod";
|
|
17
18
|
import { newLoginCode, buildLoginUrl, pollStatusOnce, decodeJwt } from "./auth.js";
|
|
18
19
|
|
|
@@ -125,20 +126,67 @@ function meetsMinVersion(version) {
|
|
|
125
126
|
// Resolve the bundled CLI from this package's OWN node_modules only — never
|
|
126
127
|
// walk up to parent directories (which may contain stale CLI versions).
|
|
127
128
|
// Returns { entry, version } or null.
|
|
128
|
-
|
|
129
|
+
//
|
|
130
|
+
// `fileURLToPath` (not `new URL(url).pathname`) is required for Windows: on a
|
|
131
|
+
// Windows `file://` URL, `.pathname` is `/C:/Users/.../src` — a leading slash
|
|
132
|
+
// before the drive letter, which is NOT a valid fs path, so `existsSync` fails
|
|
133
|
+
// and the bundled CLI is never found even though the `.mcpb` bundles it.
|
|
134
|
+
// `fileURLToPath` yields a proper `C:\Users\...\src`. macOS/Linux are unaffected.
|
|
135
|
+
//
|
|
136
|
+
// The `deps` seam (moduleUrl + path/fs impls) exists ONLY so the Windows-URL
|
|
137
|
+
// regression test can drive the win32 code path deterministically on a POSIX CI
|
|
138
|
+
// box. Production always uses the real `import.meta.url` and host path/fs.
|
|
139
|
+
export function resolveBundledCli(deps = {}) {
|
|
140
|
+
const {
|
|
141
|
+
moduleUrl = import.meta.url,
|
|
142
|
+
urlToPath = fileURLToPath,
|
|
143
|
+
pathImpl = { dirname, join },
|
|
144
|
+
fsExists = existsSync,
|
|
145
|
+
fsRead = readFileSync,
|
|
146
|
+
} = deps;
|
|
129
147
|
try {
|
|
130
|
-
const srcDir = dirname(
|
|
131
|
-
const pkgPath = join(srcDir, "..", "node_modules", "@cloudgrid-io", "cli", "package.json");
|
|
132
|
-
if (!
|
|
133
|
-
const pkg = JSON.parse(
|
|
148
|
+
const srcDir = pathImpl.dirname(urlToPath(moduleUrl));
|
|
149
|
+
const pkgPath = pathImpl.join(srcDir, "..", "node_modules", "@cloudgrid-io", "cli", "package.json");
|
|
150
|
+
if (!fsExists(pkgPath)) return null;
|
|
151
|
+
const pkg = JSON.parse(fsRead(pkgPath, "utf-8"));
|
|
134
152
|
const bin = pkg.bin && pkg.bin.cloudgrid;
|
|
135
153
|
if (!bin) return null;
|
|
136
|
-
return { entry: join(dirname(pkgPath), bin), version: pkg.version || null };
|
|
154
|
+
return { entry: pathImpl.join(pathImpl.dirname(pkgPath), bin), version: pkg.version || null };
|
|
137
155
|
} catch {
|
|
138
156
|
return null;
|
|
139
157
|
}
|
|
140
158
|
}
|
|
141
159
|
|
|
160
|
+
// Windows `.cmd`/`.bat` shims (cloudgrid.cmd, npx.cmd) cannot be launched by
|
|
161
|
+
// `execFile` without a shell — modern patched Node (CVE-2024-27980) rejects it
|
|
162
|
+
// with EINVAL, and even where it doesn't, ENOENT results because the bare name
|
|
163
|
+
// resolves to a `.cmd`. On win32 we route those through `cmd.exe /d /s /c` with
|
|
164
|
+
// an explicitly quoted command line so args with spaces/quotes stay intact.
|
|
165
|
+
// macOS/Linux never take this branch — they keep the plain execFile path.
|
|
166
|
+
const IS_WIN = process.platform === "win32";
|
|
167
|
+
|
|
168
|
+
// Quote a single Windows command-line token for cmd.exe. Wrap in double quotes,
|
|
169
|
+
// escape embedded quotes/backslash-before-quote per Windows argv rules, and also
|
|
170
|
+
// caret-escape cmd.exe metacharacters so nothing is interpreted by the shell.
|
|
171
|
+
function winQuoteArg(arg) {
|
|
172
|
+
const s = String(arg);
|
|
173
|
+
// Escape backslashes that precede a quote, then the quotes themselves.
|
|
174
|
+
let inner = s.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1");
|
|
175
|
+
inner = `"${inner}"`;
|
|
176
|
+
// Caret-escape cmd.exe special chars (they live outside the quoted-arg parsing).
|
|
177
|
+
return inner.replace(/[()%!^"<>&|]/g, "^$&");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// execFile a command that may be a Windows `.cmd` shim, safely on every OS.
|
|
181
|
+
function execMaybeCmd(command, args, options) {
|
|
182
|
+
if (!IS_WIN) return execFileAsync(command, args, options);
|
|
183
|
+
const line = [command, ...args].map(winQuoteArg).join(" ");
|
|
184
|
+
return execFileAsync(process.env.ComSpec || "cmd.exe", ["/d", "/s", "/c", line], {
|
|
185
|
+
...options,
|
|
186
|
+
windowsVerbatimArguments: true,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
142
190
|
async function runCloudgrid(args, opts = {}) {
|
|
143
191
|
const cwd = resolveCwd(opts.cwd);
|
|
144
192
|
const execOpts = {
|
|
@@ -175,11 +223,13 @@ async function runCloudgrid(args, opts = {}) {
|
|
|
175
223
|
);
|
|
176
224
|
}
|
|
177
225
|
|
|
178
|
-
// 2. Global `cloudgrid` on PATH — version-gated
|
|
226
|
+
// 2. Global `cloudgrid` on PATH — version-gated.
|
|
227
|
+
// On Windows the bin is `cloudgrid.cmd`; run it via cmd.exe (execMaybeCmd).
|
|
179
228
|
{
|
|
229
|
+
const globalBin = IS_WIN ? "cloudgrid.cmd" : "cloudgrid";
|
|
180
230
|
let useGlobal = false;
|
|
181
231
|
try {
|
|
182
|
-
const { stdout: vOut } = await
|
|
232
|
+
const { stdout: vOut } = await execMaybeCmd(globalBin, ["--version"], {
|
|
183
233
|
timeout: 5_000,
|
|
184
234
|
stdio: ["ignore", "pipe", "pipe"],
|
|
185
235
|
});
|
|
@@ -196,7 +246,7 @@ async function runCloudgrid(args, opts = {}) {
|
|
|
196
246
|
}
|
|
197
247
|
if (useGlobal) {
|
|
198
248
|
try {
|
|
199
|
-
const { stdout, stderr } = await
|
|
249
|
+
const { stdout, stderr } = await execMaybeCmd(globalBin, args, execOpts);
|
|
200
250
|
return (stdout || stderr || "").trim() || "Done.";
|
|
201
251
|
} catch (err) {
|
|
202
252
|
throw extract(err);
|
|
@@ -206,10 +256,12 @@ async function runCloudgrid(args, opts = {}) {
|
|
|
206
256
|
|
|
207
257
|
// 3. Lazy fetch: npx -y @cloudgrid-io/cli@<pinned> <args>
|
|
208
258
|
// One-time download; npx caches it for subsequent invocations.
|
|
259
|
+
// On Windows this is `npx.cmd`, run via cmd.exe (execMaybeCmd) so patched
|
|
260
|
+
// Node doesn't EINVAL on the `.cmd` shim.
|
|
209
261
|
console.error("cloudgrid-mcp: fetching the CloudGrid CLI (first use)…");
|
|
210
|
-
const npx =
|
|
262
|
+
const npx = IS_WIN ? "npx.cmd" : "npx";
|
|
211
263
|
try {
|
|
212
|
-
const { stdout, stderr } = await
|
|
264
|
+
const { stdout, stderr } = await execMaybeCmd(
|
|
213
265
|
npx,
|
|
214
266
|
["-y", CLI_NPX_PKG, ...args],
|
|
215
267
|
execOpts,
|