puppet-mcp 4.5.0 → 4.5.2

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/README.md CHANGED
@@ -7,9 +7,13 @@ manifest — it contains no server bytes.
7
7
  ## Run
8
8
 
9
9
  ```sh
10
- npx puppet-mcp@latest
10
+ npx puppet-mcp@4.5.2
11
11
  ```
12
12
 
13
+ Global installs are refused by design (`npm i -g` exits non-zero): a
14
+ global copy would shadow the pinned `npx` invocation and run a stale
15
+ engine. Always invoke the pinned version above.
16
+
13
17
  Or pin it in an MCP client as a stdio server with command and args:
14
18
 
15
19
  ```json
@@ -26,11 +30,13 @@ stdout belongs to the MCP channel.
26
30
  ## Diagnostics (no network)
27
31
 
28
32
  ```sh
29
- npx puppet-mcp@latest --doctor
33
+ npx puppet-mcp@4.5.2 --doctor
30
34
  ```
31
35
 
32
- Prints the pinned manifest and the resolved cache path, exits 0, performs
33
- no fetch. `--print-manifest` is an alias.
36
+ Prints the launcher version, the pinned manifest, the resolved engine
37
+ path, the engine version parsed from the cache directory name, and
38
+ whether they match; exits 0, performs no fetch. `--print-manifest` is
39
+ an alias. `npx puppet-mcp@4.5.2 --version` prints `puppet-mcp 4.5.2`.
34
40
 
35
41
  ## Requirements
36
42
 
@@ -39,6 +45,17 @@ no fetch. `--print-manifest` is an alias.
39
45
  - Node.js >= 22.14.0 (global `fetch` plus `crypto`/`zlib` builtins only;
40
46
  zero dependencies).
41
47
 
48
+ ## Overrides
49
+
50
+ Use `npx puppet-mcp@latest`; a pinned version is only installable while its
51
+ GitHub release exists; the one-release policy deletes older releases.
52
+
53
+ Redirect the cache with `PUPPET_ENGINE_CACHE`, or inspect the pin with no
54
+ network access (`--print-manifest` is an alias for `--doctor` above).
55
+ There is no fully offline fresh install: the first serve run still downloads
56
+ the pinned binary, so treat these as pins and overrides, not as a no-network
57
+ setup.
58
+
42
59
  ## License
43
60
 
44
61
  MIT — see LICENSE.
package/bin/puppet-mcp.js CHANGED
@@ -5,7 +5,10 @@
5
5
  //
6
6
  // Published package model (see docs/PUBLISHING.md): this tarball holds ONLY
7
7
  // this launcher plus the pinned server manifest (engine-manifest.json).
8
- // It holds NO server bytes, NO `os` field, NO `scripts`/postinstall field.
8
+ // It holds NO server bytes and NO `os` field. Its only `scripts` are the
9
+ // global-install guards (preinstall/postinstall refuse `npm i -g` so a
10
+ // global copy can never shadow a pinned `npx` run); no download happens
11
+ // at install time.
9
12
  // npm 12 blocks dependency install scripts by default (measured
10
13
  // `allow-scripts=false`), and npx has no project package.json for
11
14
  // `allowScripts` to consult, so a postinstall download would silently never
@@ -42,6 +45,7 @@ const { spawn } = require('node:child_process');
42
45
 
43
46
  const PKG_DIR = path.dirname(path.dirname(__filename));
44
47
  const MANIFEST_PATH = path.join(PKG_DIR, 'engine-manifest.json');
48
+ const PKG_PATH = path.join(PKG_DIR, 'package.json');
45
49
  const PLATFORM_OVERRIDE_ENV = 'PUPPET_MCP_ALLOW_NON_WINDOWS';
46
50
  const RUNTIME_CACHE_OVERRIDE_ENV = 'PUPPET_ENGINE_CACHE';
47
51
  const DOWNLOAD_TIMEOUT_MS = 15 * 60 * 1000;
@@ -74,6 +78,57 @@ function loadManifest() {
74
78
  }
75
79
  }
76
80
 
81
+ function loadPackageVersion() {
82
+ let raw;
83
+ try {
84
+ raw = fs.readFileSync(PKG_PATH, 'utf8');
85
+ } catch (err) {
86
+ fail(`cannot read package.json at ${PKG_PATH}: ${err.message}`);
87
+ }
88
+ let pkg;
89
+ try {
90
+ pkg = JSON.parse(raw);
91
+ } catch (err) {
92
+ fail(`package.json is not valid JSON: ${err.message}`);
93
+ }
94
+ if (pkg.name !== 'puppet-mcp' || typeof pkg.version !== 'string' || pkg.version.length === 0) {
95
+ fail('package.json does not name a versioned puppet-mcp package.');
96
+ }
97
+ return pkg.version;
98
+ }
99
+
100
+ // Engine cache leaf dirs are `<semver>-<bundleSha8>` (or `<semver>-unpublished`
101
+ // before the release workflow fills the manifest hashes).
102
+ function isEngineLeafName(base) {
103
+ return /^\d+\.\d+\.\d+-(?:[0-9a-fA-F]{8}|unpublished)$/.test(base);
104
+ }
105
+
106
+ // The semver embedded in an engine cache path, or null when the path names
107
+ // no engine leaf.
108
+ function parseEngineVersionFromDir(dir) {
109
+ const m = /^(\d+\.\d+\.\d+)-(?:[0-9a-fA-F]{8}|unpublished)$/.exec(path.basename(dir));
110
+ return m ? m[1] : null;
111
+ }
112
+
113
+ // Fail closed on version skew: never spawn an engine older (or newer) than
114
+ // this launcher. Prints one actionable line and exits non-zero.
115
+ function assertVersionMatch(pkgVersion, manifest, cacheDir) {
116
+ const reprovision = `npx -y puppet-mcp@${pkgVersion}`;
117
+ if (manifest.serverVersion !== pkgVersion) {
118
+ fail(
119
+ `version mismatch: launcher ${pkgVersion} but manifest pins server ${manifest.serverVersion}; ` +
120
+ `re-provision with \`${reprovision}\` from a clean cache.`
121
+ );
122
+ }
123
+ const engineVersion = parseEngineVersionFromDir(cacheDir);
124
+ if (engineVersion !== null && engineVersion !== pkgVersion) {
125
+ fail(
126
+ `version mismatch: launcher ${pkgVersion} but engine ${engineVersion} at ${cacheDir}; ` +
127
+ `re-provision with \`${reprovision}\` (unset ${RUNTIME_CACHE_OVERRIDE_ENV} if it names the stale engine).`
128
+ );
129
+ }
130
+ }
131
+
77
132
  // A sha256 slot the release workflow has not filled yet. The committed
78
133
  // manifest carries the sentinel for every hash computed at release time;
79
134
  // the workflow replaces each with the real 64-hex digest. Anything else
@@ -91,6 +146,12 @@ function shortHash(sha) {
91
146
  function resolveRuntimeDir(manifest) {
92
147
  const notes = [];
93
148
  let root = process.env[RUNTIME_CACHE_OVERRIDE_ENV];
149
+ // An override that already names an engine leaf dir (<semver>-<sha8>)
150
+ // is used as-is, so a stale pin resolves to the stale engine and the
151
+ // version gate below can refuse it instead of silently provisioning past.
152
+ if (root && isEngineLeafName(path.basename(path.resolve(root)))) {
153
+ return { dir: path.resolve(root), overridden: true, notes };
154
+ }
94
155
  let overridden = Boolean(root);
95
156
  if (!root) {
96
157
  root = process.env.LOCALAPPDATA;
@@ -101,11 +162,16 @@ function resolveRuntimeDir(manifest) {
101
162
  return { dir: path.join(root, 'puppet', 'engine', leaf), overridden, notes };
102
163
  }
103
164
 
104
- function printDoctor(manifest) {
165
+ function printDoctor(manifest, pkgVersion) {
105
166
  const runtime = resolveRuntimeDir(manifest);
167
+ const engineVersion = parseEngineVersionFromDir(runtime.dir);
106
168
  const report = {
107
169
  package: manifest.package,
170
+ launcherVersion: pkgVersion,
108
171
  serverVersion: manifest.serverVersion,
172
+ engineVersion,
173
+ enginePath: path.join(runtime.dir, manifest.runtime.server.name),
174
+ versionsMatch: engineVersion === pkgVersion && manifest.serverVersion === pkgVersion,
109
175
  platform: process.platform,
110
176
  node: process.version,
111
177
  runtimeCacheDir: runtime.dir,
@@ -120,9 +186,10 @@ function printDoctor(manifest) {
120
186
 
121
187
  function usage(exitCode, stream) {
122
188
  const text = [
123
- 'usage: puppet-mcp [--doctor | --print-manifest | --help]',
189
+ 'usage: puppet-mcp [--version | --doctor | --print-manifest | --help]',
124
190
  '',
125
191
  ' (no args) provision the pinned server on first run, then serve MCP over stdio',
192
+ ' --version print `puppet-mcp <launcherVersion>`, exit 0',
126
193
  ' --doctor, print the pinned manifest and the resolved cache',
127
194
  ' --print-manifest path, perform no network access, exit 0',
128
195
  ' --help, -h print this usage',
@@ -385,12 +452,20 @@ async function main() {
385
452
  const args = process.argv.slice(2);
386
453
  const manifest = loadManifest();
387
454
  if (manifest.package !== 'puppet-mcp') fail('engine manifest names the wrong package.');
455
+ const pkgVersion = loadPackageVersion();
388
456
  if (args.includes('--help') || args.includes('-h')) usage(0, process.stdout);
457
+ if (args.includes('--version')) {
458
+ const known = new Set(['--version']);
459
+ const unknown = args.filter((a) => !known.has(a));
460
+ if (unknown.length > 0) usage(2, process.stderr);
461
+ process.stdout.write(`puppet-mcp ${pkgVersion}\n`);
462
+ process.exit(0);
463
+ }
389
464
  if (args.includes('--doctor') || args.includes('--print-manifest')) {
390
465
  const known = new Set(['--doctor', '--print-manifest']);
391
466
  const unknown = args.filter((a) => !known.has(a));
392
467
  if (unknown.length > 0) usage(2, process.stderr);
393
- printDoctor(manifest); // exits 0; no network on this path by construction
468
+ printDoctor(manifest, pkgVersion); // exits 0; no network on this path by construction
394
469
  }
395
470
  // Every known flag returns above; anything left is unknown.
396
471
  if (args.length > 0) usage(2, process.stderr);
@@ -406,6 +481,9 @@ async function main() {
406
481
  if (runtime.dir.startsWith('<unresolvable>')) {
407
482
  fail(`cannot resolve runtime cache dir: ${runtime.notes.join('; ')}. Set ${RUNTIME_CACHE_OVERRIDE_ENV} to an absolute directory.`);
408
483
  }
484
+ // Fail closed BEFORE any download or spawn: a stale cache path (e.g. via
485
+ // override) or a mispublished manifest refuses here, never runs.
486
+ assertVersionMatch(pkgVersion, manifest, runtime.dir);
409
487
  const readyRuntime = await ensureRuntime(manifest, runtime.dir);
410
488
  runServer(readyRuntime, manifest);
411
489
  }
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "_comment": "Pinned server manifest for the puppet-mcp launcher. The published package holds ONLY this launcher plus this manifest (no server bytes). On first run the launcher downloads ONLY runtime.archive from the tagged GitHub Release, verifying its sha256 before and after extraction. The release workflow fills the runtime.* hashes it computes from the real build artifacts; the committed manifest carries placeholders the launcher refuses to run with.",
3
3
  "package": "puppet-mcp",
4
- "serverVersion": "4.5.0",
4
+ "serverVersion": "4.5.2",
5
5
  "runtime": {
6
- "source": "https://github.com/nathwn12/puppet/releases/download/v4.5.0",
6
+ "source": "https://github.com/nathwn12/puppet/releases/download/v4.5.2",
7
7
  "archive": {
8
- "name": "puppet-v4.5.0-windows-x86_64.zip",
9
- "sha256": "bfae663ba35106573b878eb17679692b4a675a044761451402b7e73b0f954a0c",
10
- "size": 687934
8
+ "name": "puppet-v4.5.2-windows-x86_64.zip",
9
+ "sha256": "4b0ebbf25194530fd426d6dcc1fbb54270cc4a1b8148d1ffedd5fc2f94fc83ea",
10
+ "size": 687786
11
11
  },
12
12
  "server": {
13
13
  "name": "puppet.exe",
14
- "sha256": "71aed3dde1731a83b8148c449905685446d13568a0eb7fd0cd670737576cc76a",
14
+ "sha256": "c0fb32a61583084eb0e54a6c9f5be1c2d4c85eac3761256ae547b5ae850a2e09",
15
15
  "size": 1810944
16
16
  }
17
17
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "puppet-mcp",
3
- "version": "4.5.0",
3
+ "version": "4.5.2",
4
4
  "description": "Windows desktop-control MCP server (npx launcher): provisions the pinned server binary from the GitHub Release on first run, then serves MCP over stdio",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -14,6 +14,10 @@
14
14
  "bin": {
15
15
  "puppet-mcp": "./bin/puppet-mcp.js"
16
16
  },
17
+ "scripts": {
18
+ "preinstall": "node -e \"if(process.env.npm_config_global==='true'){console.error('puppet-mcp: global install refused — use npx puppet-mcp@4.5.2');process.exit(1)}\"",
19
+ "postinstall": "node -e \"if(process.env.npm_config_global==='true'){console.error('puppet-mcp: global install refused — use npx puppet-mcp@4.5.2');process.exit(1)}\""
20
+ },
17
21
  "files": [
18
22
  "bin/",
19
23
  "engine-manifest.json",