sentinel-ir-mcp 0.5.3 → 0.5.4

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
@@ -28,6 +28,14 @@ of safety. Other languages are not supported yet. What is detected in Python
28
28
  and Rust (and what is not) is listed in the
29
29
  [sentinel-ir README](https://gitlab.com/jakubrojicek11/sentinel-ir#python).
30
30
 
31
+ **0.5.4** makes `doctor` notice an old copy of itself: it asks npm for the
32
+ newest release on its `stable-X.Y` tag (3 s timeout, skipped offline) and,
33
+ when that is newer, warns and says where the old copy runs from (npx cache,
34
+ global install or another folder) and how to replace it. Before, `npx
35
+ sentinel-ir-mcp doctor` without a version could run an old copy and report
36
+ `[ok]`. The last line now counts warnings (`All checks passed, 1 warning
37
+ above.`).
38
+
31
39
  **0.5.3** splits each tool description into short lines (what it returns,
32
40
  parameters, risk scale, when to use another tool) and names the sibling tools
33
41
  in all three; `analyze_code` no longer repeats the language rule its schema
@@ -257,7 +265,8 @@ without a shell failed with `ENOENT` on a real Windows runner. For VS Code and
257
265
  Claude Code, `setup` prints a PowerShell and a Command Prompt version of the
258
266
  command. What was measured on which system: [COMPATIBILITY.md](COMPATIBILITY.md).
259
267
 
260
- `doctor` checks Node.js, the package version, the client config (valid JSON, a
268
+ `doctor` checks Node.js, the package version (and asks the npm registry, or
269
+ `npm_config_registry`, whether its `stable-X.Y` tag has a newer release), the client config (valid JSON, a
261
270
  `"sentinel-ir"` entry that starts `sentinel-ir-mcp`, existing folders), then
262
271
  starts **its own** server with the same folders, performs a real MCP
263
272
  handshake, expects exactly the three tools and calls `analyze_directory` with
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinel-ir-mcp",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "mcpName": "io.github.jakcmencz/sentinel-ir-mcp",
5
5
  "description": "MCP server for sentinel-ir: lets AI agents (Claude Desktop, Cursor, Windsurf, ...) read a compact structural IR of JavaScript, TypeScript, Python and Rust files instead of the raw source. Read-only, sandboxed to allowed folders.",
6
6
  "main": "src/server.js",
package/src/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const readline = require("readline");
4
4
  const { runSetup } = require("./setup");
5
- const { runDoctor } = require("./doctor");
5
+ const { runDoctor, fetchDistTags } = require("./doctor");
6
6
  const { GUIDE } = require("./guide");
7
7
  const { runLog } = require("./logView");
8
8
  const { CLIENT_NAMES, PACKAGE_SPEC } = require("./clients");
@@ -103,6 +103,7 @@ function defaultIo() {
103
103
  env: process.env,
104
104
  platform: process.platform,
105
105
  cwd: process.cwd(),
106
+ fetchDistTags,
106
107
  isTTY,
107
108
  prompt: isTTY ? (question) => new Promise((resolve) => {
108
109
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
package/src/doctor.js CHANGED
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  const fs = require("fs");
4
+ const http = require("http");
5
+ const https = require("https");
4
6
  const path = require("path");
5
7
  const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
6
8
  const { StdioClientTransport } = require("@modelcontextprotocol/sdk/client/stdio.js");
@@ -44,12 +46,80 @@ function readVersion(file) {
44
46
  }
45
47
  }
46
48
 
47
- // The _npx/<key> folder this copy was started from, or null.
48
- function npxCopyDir() {
49
- const m = new RegExp(`^(.*\\${path.sep}_npx\\${path.sep}[^\\${path.sep}]+)\\${path.sep}`).exec(__dirname);
49
+ const PACKAGE_DIR = path.join(__dirname, "..");
50
+
51
+ // The _npx/<key> folder the package in dir was started from, or null.
52
+ function npxCopyDir(dir = PACKAGE_DIR) {
53
+ const m = /^(.*[\\/]_npx[\\/][^\\/]+)[\\/]/.exec(dir);
50
54
  return m ? m[1] : null;
51
55
  }
52
56
 
57
+ // Where the package in dir is installed: an npx cache copy, a global install or elsewhere.
58
+ function installKind(dir) {
59
+ const npx = npxCopyDir(dir);
60
+ if (npx) return { kind: "npx", dir: npx, key: /[^\\/]+$/.exec(npx)[0] };
61
+ if (/[\\/](lib|npm)[\\/]node_modules[\\/]sentinel-ir-mcp[\\/]?$/i.test(dir)) return { kind: "global", dir };
62
+ return { kind: "other", dir };
63
+ }
64
+
65
+ // npm dist-tags of sentinel-ir-mcp, or null when the registry does not answer in time.
66
+ function fetchDistTags(env, timeoutMs = 3000) {
67
+ return new Promise((resolve) => {
68
+ let url;
69
+ try {
70
+ url = new URL("-/package/sentinel-ir-mcp/dist-tags", (env.npm_config_registry || "https://registry.npmjs.org/").replace(/\/?$/, "/"));
71
+ } catch { resolve(null); return; }
72
+ const lib = url.protocol === "http:" ? http : url.protocol === "https:" ? https : null;
73
+ if (!lib) { resolve(null); return; }
74
+ const req = lib.get(url, { timeout: timeoutMs, headers: { accept: "application/json" } }, (res) => {
75
+ if (res.statusCode !== 200) { res.resume(); resolve(null); return; }
76
+ let body = "";
77
+ res.setEncoding("utf8");
78
+ res.on("data", (d) => { body += d; if (body.length > 65536) { resolve(null); req.destroy(); } });
79
+ res.on("end", () => {
80
+ try {
81
+ const tags = JSON.parse(body);
82
+ resolve(tags && typeof tags === "object" && !Array.isArray(tags) ? tags : null);
83
+ } catch { resolve(null); }
84
+ });
85
+ });
86
+ const timer = setTimeout(() => req.destroy(), timeoutMs);
87
+ req.on("timeout", () => req.destroy());
88
+ req.on("error", () => resolve(null));
89
+ req.on("close", () => { clearTimeout(timer); resolve(null); });
90
+ });
91
+ }
92
+
93
+ // Warns when npm has a newer release on this version's stable-X.Y tag; says where this copy runs from.
94
+ async function checkUpdate(io, add) {
95
+ if (typeof io.fetchDistTags !== "function") return;
96
+ const m = /^(\d+)\.(\d+)\./.exec(version);
97
+ const tag = m ? `stable-${m[1]}.${m[2]}` : "latest";
98
+ const tags = await io.fetchDistTags(io.env);
99
+ if (!tags) {
100
+ add("skip", "package.update", "Could not reach the npm registry to look for a newer sentinel-ir-mcp (offline or blocked)");
101
+ return;
102
+ }
103
+ const newest = tags[tag];
104
+ if (typeof newest !== "string" || !/^\d+\.\d+\.\d+$/.test(newest)) {
105
+ add("skip", "package.update", `npm has no ${tag} tag for sentinel-ir-mcp`);
106
+ return;
107
+ }
108
+ if (!olderThan(version, newest)) {
109
+ add("ok", "package.update", `sentinel-ir-mcp ${version} is the newest ${tag} on npm`);
110
+ return;
111
+ }
112
+ const spec = `sentinel-ir-mcp@${tag}`;
113
+ const from = installKind(io.packageDir || PACKAGE_DIR);
114
+ const where = { npx: `the npx cache (${from.dir})`, global: `a global install (${from.dir})`, other: from.dir }[from.kind];
115
+ const fix = {
116
+ npx: `Close the client, delete that folder (npm 11: npm cache npx rm ${from.key}), then run: npx -y ${spec} setup`,
117
+ global: `Run: npm install -g ${spec} (or npm uninstall -g sentinel-ir-mcp), then: npx -y ${spec} doctor`,
118
+ other: `Run the current version: npx -y ${spec} doctor`
119
+ }[from.kind];
120
+ add("warn", "package.update", `This is sentinel-ir-mcp ${version}, npm has ${newest} (${tag}); this copy runs from ${where}`, fix);
121
+ }
122
+
53
123
  // npm's cache folder: the one this copy was started from by npx, else npm's default.
54
124
  function npmCacheDir(env, platform) {
55
125
  const marker = `${path.sep}_npx${path.sep}`;
@@ -279,6 +349,7 @@ async function runDoctor(opts, io) {
279
349
  add("fail", "package", `sentinel-ir-mcp ${version} runs sentinel-ir ${ir}, needs ${IR_MIN} or newer`,
280
350
  npxCopyDir() ? `Close the client, delete ${npxCopyDir()}, then run: npx -y ${PACKAGE_SPEC} setup` : `Reinstall it so that npm installs sentinel-ir ${dependencies["sentinel-ir"]}.`);
281
351
  } else add("ok", "package", `sentinel-ir-mcp ${version}, sentinel-ir ${ir}`);
352
+ await checkUpdate(io, add);
282
353
  checkNpxCache(io.env, io.platform, add);
283
354
 
284
355
  const project = path.resolve(io.cwd, opts.project || ".");
@@ -323,9 +394,11 @@ async function runDoctor(opts, io) {
323
394
  io.stdout.write(JSON.stringify({ ok: !failed, checks }, null, 2) + "\n");
324
395
  } else {
325
396
  for (const c of checks) io.stdout.write(`${MARK[c.status]} ${c.message}\n${c.fix ? ` -> ${c.fix}\n` : ""}`);
326
- io.stdout.write(failed ? "\nSome checks failed.\n" : "\nAll checks passed.\n");
397
+ const warnings = checks.filter((c) => c.status === "warn").length;
398
+ const warned = warnings ? `, ${warnings} warning${warnings > 1 ? "s" : ""} above` : "";
399
+ io.stdout.write(failed ? `\nSome checks failed${warned}.\n` : `\nAll checks passed${warned}.\n`);
327
400
  }
328
401
  return failed ? 1 : 0;
329
402
  }
330
403
 
331
- module.exports = { runDoctor, entryFolders, olderThan, staleNpxCopies, IR_MIN };
404
+ module.exports = { runDoctor, entryFolders, olderThan, staleNpxCopies, fetchDistTags, installKind, IR_MIN };