mcp-server-mcpindex 0.2.2 → 0.3.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/CHANGELOG.md +15 -0
- package/package.json +2 -2
- package/src/index.mjs +13 -2
- package/src/update-check.mjs +120 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,21 @@ All notable changes to `mcp-server-mcpindex` are recorded here.
|
|
|
4
4
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.3.0] - 2026-06-08
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Update notice on startup.** After the server connects, it does a best-effort check against the npm registry and, if a newer version is published, prints one advisory line to stderr and (if the host enabled MCP logging) sends a `notifications/message` the host may render — including the reminder to **restart your MCP host to load the new version**. The check is time-boxed (~2.5s), fail-silent on any error, fire-and-forget (it can never delay or crash startup), sends no telemetry beyond the standard registry GET, and never follows a redirect off the pinned registry host. Opt out with `MCPINDEX_NO_UPDATE_CHECK=1`.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **The running version is now read from `package.json`** instead of a hardcoded constant (which had drifted to `0.2.1` while the package was `0.2.2`), so the User-Agent and the update check always reflect what npm actually shipped.
|
|
16
|
+
- The `logging` server capability is now declared (required to deliver the update notice as an MCP notification).
|
|
17
|
+
|
|
18
|
+
### Internal
|
|
19
|
+
|
|
20
|
+
- New `src/update-check.mjs` (pure, injected-`fetch`, unit-tested) + `test/update-check.test.mjs`. `npm run build` now syntax-checks it too.
|
|
21
|
+
|
|
7
22
|
## [0.2.2] - 2026-05-31
|
|
8
23
|
|
|
9
24
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-server-mcpindex",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "An MCP server for finding MCP servers, plus advisory trust verdicts (check_tool_trust, assess_server) for agent frameworks. Drop-in for Claude Desktop, Cursor, Cline, Zed.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"LICENSE"
|
|
40
40
|
],
|
|
41
41
|
"scripts": {
|
|
42
|
-
"build": "node --check src/index.mjs && node --check src/trust.mjs",
|
|
42
|
+
"build": "node --check src/index.mjs && node --check src/trust.mjs && node --check src/update-check.mjs",
|
|
43
43
|
"test": "node --test test/*.test.mjs",
|
|
44
44
|
"check-published": "node scripts/check-published.mjs",
|
|
45
45
|
"release": "node scripts/release.mjs"
|
package/src/index.mjs
CHANGED
|
@@ -8,14 +8,20 @@ import {
|
|
|
8
8
|
CallToolRequestSchema,
|
|
9
9
|
ListToolsRequestSchema,
|
|
10
10
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
11
12
|
export { checkToolTrust, assessServer, VERDICT_CONTRACT_VERSION, V1_HONEST_LIMITS } from './trust.mjs';
|
|
13
|
+
import { notifyUpdateIfAvailable } from './update-check.mjs';
|
|
12
14
|
|
|
13
15
|
const API_BASE = process.env.MCPINDEX_API_BASE ?? 'https://mcpindex.ai';
|
|
14
|
-
|
|
16
|
+
// Single source of truth for the running version — read from package.json so the
|
|
17
|
+
// User-Agent and the update-check can never drift from what npm actually shipped.
|
|
18
|
+
const PKG_VERSION = createRequire(import.meta.url)('../package.json').version;
|
|
15
19
|
|
|
16
20
|
const server = new Server(
|
|
17
21
|
{ name: 'mcp-server-mcpindex', version: PKG_VERSION },
|
|
18
|
-
|
|
22
|
+
// `logging` lets us push the update notice as an MCP notifications/message the
|
|
23
|
+
// host may render to the user; `sendLoggingMessage` no-ops without it.
|
|
24
|
+
{ capabilities: { tools: {}, logging: {} } },
|
|
19
25
|
);
|
|
20
26
|
|
|
21
27
|
const TOOLS = [
|
|
@@ -284,3 +290,8 @@ function formatCompare(rows) {
|
|
|
284
290
|
const transport = new StdioServerTransport();
|
|
285
291
|
await server.connect(transport);
|
|
286
292
|
console.error('[mcp-server-mcpindex] connected via stdio');
|
|
293
|
+
|
|
294
|
+
// Fire-and-forget: tell the user if a newer version exists (stderr + best-effort
|
|
295
|
+
// MCP notification). Dropped onto the event loop AFTER connect so it can never
|
|
296
|
+
// delay or crash startup; all errors are swallowed inside.
|
|
297
|
+
notifyUpdateIfAvailable({ currentVersion: PKG_VERSION, server }).catch(() => {});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// update-check.mjs — best-effort "a newer mcp-server-mcpindex exists" notice.
|
|
2
|
+
//
|
|
3
|
+
// Why this is safe to ship in a server that already egresses:
|
|
4
|
+
// - outbound to the PINNED npm registry host only, https, no redirect-following
|
|
5
|
+
// (no SSRF pivot), time-boxed, and fail-silent on ANY error;
|
|
6
|
+
// - fire-and-forget — it never blocks or crashes startup (the caller drops it
|
|
7
|
+
// onto the event loop after connect and ignores rejections);
|
|
8
|
+
// - no telemetry beyond the standard registry GET (no version of the user, no
|
|
9
|
+
// machine id — just "what's the latest published version of this package");
|
|
10
|
+
// - opt-out via MCPINDEX_NO_UPDATE_CHECK.
|
|
11
|
+
//
|
|
12
|
+
// Pure + injectable (fetchImpl) so it is unit-testable with no network.
|
|
13
|
+
|
|
14
|
+
const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
|
|
15
|
+
const DEFAULT_PKG = 'mcp-server-mcpindex';
|
|
16
|
+
const DEFAULT_TIMEOUT_MS = 2500;
|
|
17
|
+
|
|
18
|
+
/** major.minor.patch -> [n,n,n]; drops prerelease/build; 9-digit cap (DoS guard).
|
|
19
|
+
* Returns null for anything non-canonical (so an odd tag never triggers a nag). */
|
|
20
|
+
export function parseSemver(v) {
|
|
21
|
+
if (typeof v !== 'string') return null;
|
|
22
|
+
const core = v.trim().split('+')[0].split('-')[0];
|
|
23
|
+
const parts = core.split('.');
|
|
24
|
+
if (parts.length !== 3) return null;
|
|
25
|
+
const nums = [];
|
|
26
|
+
for (const p of parts) {
|
|
27
|
+
if (!/^\d{1,9}$/.test(p)) return null;
|
|
28
|
+
nums.push(Number(p));
|
|
29
|
+
}
|
|
30
|
+
return nums;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** true iff `current` is a strictly-older canonical semver than `latest`.
|
|
34
|
+
* Unparseable on either side -> false (never nag on ambiguity). */
|
|
35
|
+
export function isOlder(current, latest) {
|
|
36
|
+
const a = parseSemver(current);
|
|
37
|
+
const b = parseSemver(latest);
|
|
38
|
+
if (!a || !b) return false;
|
|
39
|
+
for (let i = 0; i < 3; i += 1) {
|
|
40
|
+
if (a[i] < b[i]) return true;
|
|
41
|
+
if (a[i] > b[i]) return false;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The user-facing line. Advisory voice; carries the restart reminder because a
|
|
47
|
+
* running MCP server is only replaced when the host restarts it. */
|
|
48
|
+
export function formatUpdateMessage(current, latest) {
|
|
49
|
+
return (
|
|
50
|
+
`mcp-server-mcpindex ${current} -> ${latest} available. ` +
|
|
51
|
+
`Update: npm i -g mcp-server-mcpindex@latest ` +
|
|
52
|
+
`(or npx fetches it on next launch), then restart your MCP host ` +
|
|
53
|
+
`(Claude Desktop / Cursor / Cline / Zed) to load it.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Disabled when MCPINDEX_NO_UPDATE_CHECK is set to any non-falsey value. */
|
|
58
|
+
export function updateCheckDisabled(env = process.env) {
|
|
59
|
+
const raw = (env.MCPINDEX_NO_UPDATE_CHECK ?? '').trim().toLowerCase();
|
|
60
|
+
return raw !== '' && raw !== '0' && raw !== 'false' && raw !== 'no' && raw !== 'off';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Fetch the latest published version string, or null on ANY failure/timeout.
|
|
64
|
+
* Never throws. `fetchImpl` is injectable for tests. */
|
|
65
|
+
export async function fetchLatestVersion({
|
|
66
|
+
registry = DEFAULT_REGISTRY,
|
|
67
|
+
pkg = DEFAULT_PKG,
|
|
68
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
69
|
+
fetchImpl = fetch,
|
|
70
|
+
} = {}) {
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetchImpl(`${registry}/${encodeURIComponent(pkg)}/latest`, {
|
|
75
|
+
signal: controller.signal,
|
|
76
|
+
headers: { Accept: 'application/json' },
|
|
77
|
+
redirect: 'error', // SSRF: never follow a redirect off the pinned host
|
|
78
|
+
});
|
|
79
|
+
if (!res || !res.ok) return null;
|
|
80
|
+
const body = await res.json();
|
|
81
|
+
const v = body && typeof body.version === 'string' ? body.version : null;
|
|
82
|
+
return v && parseSemver(v) ? v : null;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
} finally {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Orchestration: check, and if a newer version exists, emit on BOTH channels.
|
|
91
|
+
* stderr is the reliable channel; the MCP logging notification is best-effort
|
|
92
|
+
* (only delivered if the host enabled `logging` and didn't filter `info`).
|
|
93
|
+
* Returns the message it emitted (or null) — handy for tests/callers.
|
|
94
|
+
* Never throws; never blocks the caller meaningfully (caller fire-and-forgets). */
|
|
95
|
+
export async function notifyUpdateIfAvailable({
|
|
96
|
+
currentVersion,
|
|
97
|
+
server = null,
|
|
98
|
+
env = process.env,
|
|
99
|
+
log = (m) => console.error(`[mcp-server-mcpindex] ${m}`),
|
|
100
|
+
fetchImpl = fetch,
|
|
101
|
+
pkg = DEFAULT_PKG,
|
|
102
|
+
} = {}) {
|
|
103
|
+
try {
|
|
104
|
+
if (updateCheckDisabled(env)) return null;
|
|
105
|
+
const latest = await fetchLatestVersion({ pkg, fetchImpl });
|
|
106
|
+
if (!latest || !isOlder(currentVersion, latest)) return null;
|
|
107
|
+
const msg = formatUpdateMessage(currentVersion, latest);
|
|
108
|
+
log(msg); // stderr — reliable
|
|
109
|
+
if (server && typeof server.sendLoggingMessage === 'function') {
|
|
110
|
+
try {
|
|
111
|
+
await server.sendLoggingMessage({ level: 'info', logger: 'mcpindex', data: msg });
|
|
112
|
+
} catch {
|
|
113
|
+
/* host didn't enable logging / filtered the level — stderr already covered it */
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return msg;
|
|
117
|
+
} catch {
|
|
118
|
+
return null; // belt-and-suspenders: this path must never surface an error
|
|
119
|
+
}
|
|
120
|
+
}
|