mcp-server-mcpindex 0.2.1 → 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 CHANGED
@@ -4,6 +4,32 @@ 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
+
22
+ ## [0.2.2] - 2026-05-31
23
+
24
+ ### Changed
25
+
26
+ - **Repository metadata now points at the monorepo source.** `repository` is `mcpindex-ai/mcpindex-web` with `"directory": "mcp-server-mcpindex"` (the npm-standard convention for a package whose source lives in a monorepo subdirectory), and `bugs` points at that repo's issues. The previously-linked standalone repo `mcpindex-ai/mcp-server-mcpindex` was an orphaned `0.1.0` snapshot (pre-trust, no `trust.mjs`); it has been archived in favor of a single source of truth. No code change - `npm install` behaves identically to `0.2.1`.
27
+
28
+ ### Added
29
+
30
+ - **`npm run release`** - one-command release (syntax check, tests, CHANGELOG-entry assertion, version, publish, tag) so the source-to-npm step can no longer drift by being done by hand.
31
+ - **`npm run check-published`** - fails when the in-tree version is ahead of the published npm version (catches "edited but forgot to publish").
32
+
7
33
  ## [0.2.1] - 2026-05-29
8
34
 
9
35
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-server-mcpindex",
3
- "version": "0.2.1",
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",
@@ -16,10 +16,11 @@
16
16
  "homepage": "https://mcpindex.ai",
17
17
  "repository": {
18
18
  "type": "git",
19
- "url": "https://github.com/mcpindex-ai/mcp-server-mcpindex"
19
+ "url": "https://github.com/mcpindex-ai/mcpindex-web",
20
+ "directory": "mcp-server-mcpindex"
20
21
  },
21
22
  "bugs": {
22
- "url": "https://github.com/mcpindex-ai/mcp-server-mcpindex/issues"
23
+ "url": "https://github.com/mcpindex-ai/mcpindex-web/issues"
23
24
  },
24
25
  "author": "Bhartis LLC <hello@mcpindex.ai>",
25
26
  "license": "MIT",
@@ -38,8 +39,10 @@
38
39
  "LICENSE"
39
40
  ],
40
41
  "scripts": {
41
- "build": "node --check src/index.mjs && node --check src/trust.mjs",
42
- "test": "node --test test/*.test.mjs"
42
+ "build": "node --check src/index.mjs && node --check src/trust.mjs && node --check src/update-check.mjs",
43
+ "test": "node --test test/*.test.mjs",
44
+ "check-published": "node scripts/check-published.mjs",
45
+ "release": "node scripts/release.mjs"
43
46
  },
44
47
  "dependencies": {
45
48
  "@modelcontextprotocol/sdk": "^1.0.0"
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
- const PKG_VERSION = '0.2.1';
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
- { capabilities: { tools: {} } },
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
+ }