@extenshi/mcp
Version:
Extenshi MCP server — bring the extension catalog (search, security, market research) and pre-publish scanning into your AI tools
85 lines • 3.37 kB
JavaScript
/**
* Best-effort "a newer version is available" nudge.
*
* The MCP has no built-in update mechanism — a pinned global install
* (`npm i -g @extenshi/mcp`) stays on whatever version was installed, and even
* an `npx @extenshi/mcp` (no `@latest`) can serve a stale cache. So on startup
* we ask the npm registry once for the latest published version and, if we're
* behind, print a one-line hint to STDERR.
*
* Hard constraints:
* - STDERR only. stdout is the MCP protocol channel (see index.ts header).
* - Fully fail-soft: any network/parse/timeout error is swallowed silently.
* - Non-blocking: callers fire-and-forget so tool availability is never delayed.
* - Opt-out honoured (shares the telemetry opt-out switches + a dedicated one).
*/
const REGISTRY_URL = 'https://registry.npmjs.org/@extenshi/mcp/latest';
const TIMEOUT_MS = 2500;
/** True when the user has opted out of the (network) update check. */
export function updateCheckOptedOut(env = process.env) {
return (env.EXTENSHI_NO_UPDATE_CHECK === '1' ||
env.DO_NOT_TRACK === '1' ||
env.EXTENSHI_TELEMETRY === '0' ||
env.CI === 'true');
}
/**
* Returns true when `latest` is a strictly newer 3-part version than `current`.
* Tolerant of pre-release/garbage: non-numeric parts coerce to 0, and any parse
* oddity returns false (never nag spuriously).
*/
export function isNewerVersion(current, latest) {
const parse = (v) => v
.split('.')
.slice(0, 3)
.map((p) => Number.parseInt(p, 10));
const a = parse(current);
const b = parse(latest);
for (let i = 0; i < 3; i++) {
const x = Number.isFinite(a[i]) ? a[i] : 0;
const y = Number.isFinite(b[i]) ? b[i] : 0;
if (y > x)
return true;
if (y < x)
return false;
}
return false;
}
/** Build the stderr hint (exported for testability). */
export function updateMessage(current, latest) {
return (`[extenshi-mcp] A newer version is available: ${current} → ${latest}. ` +
'If your client runs `npx -y @extenshi/mcp@latest`, just restart it. ' +
'For a pinned global install: `npm i -g @extenshi/mcp@latest`, then restart. ' +
'(Silence this with EXTENSHI_NO_UPDATE_CHECK=1.)\n');
}
/**
* Fire-and-forget update check. Never throws; writes at most one line to stderr.
* `fetchImpl`/`out` are injectable for tests.
*/
export async function checkForUpdate(current, fetchImpl = fetch, out = (m) => process.stderr.write(m)) {
if (updateCheckOptedOut())
return;
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
let res;
try {
res = await fetchImpl(REGISTRY_URL, {
signal: controller.signal,
headers: { accept: 'application/json' },
});
}
finally {
clearTimeout(timer);
}
if (!res.ok)
return;
const json = (await res.json());
const latest = typeof json.version === 'string' ? json.version : undefined;
if (latest && isNewerVersion(current, latest))
out(updateMessage(current, latest));
}
catch {
// Offline, aborted, non-JSON, registry hiccup — stay silent.
}
}
//# sourceMappingURL=update-check.js.map