@remcp/remcp 0.2.4 → 0.2.6
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/package.json +1 -1
- package/src/agent.mjs +64 -1
- package/src/cli.mjs +27 -2
package/package.json
CHANGED
package/src/agent.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import os from 'node:os';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import process from 'node:process';
|
|
5
|
-
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
6
6
|
import WebSocket from 'ws';
|
|
7
7
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
8
8
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
@@ -10,6 +10,8 @@ import { normalizeRuntime } from './runtime.mjs';
|
|
|
10
10
|
import { VERSION } from './version.mjs';
|
|
11
11
|
|
|
12
12
|
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
13
|
+
const UPDATE_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
14
|
+
const UPDATE_CHECK_TIMEOUT_MS = 5000;
|
|
13
15
|
const METRICS_INTERVAL_MS = 60_000;
|
|
14
16
|
const TELEMETRY_QUEUE_LIMIT = 500;
|
|
15
17
|
const TELEMETRY_BATCH_LIMIT = 100;
|
|
@@ -35,6 +37,31 @@ function localRuntimeEntry(runtimeValue) {
|
|
|
35
37
|
return candidate;
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
function parseVersion(value) {
|
|
41
|
+
const match = String(value || '').match(/(\d+)\.(\d+)\.(\d+)/);
|
|
42
|
+
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isNewer(candidate, current) {
|
|
46
|
+
const a = parseVersion(candidate);
|
|
47
|
+
const b = parseVersion(current);
|
|
48
|
+
if (!a || !b) return false;
|
|
49
|
+
for (let index = 0; index < 3; index += 1) {
|
|
50
|
+
if (a[index] > b[index]) return true;
|
|
51
|
+
if (a[index] < b[index]) return false;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function globalCliEntry() {
|
|
57
|
+
const prefix = spawnSync(npmCommand, ['prefix', '--global'], { encoding: 'utf8' });
|
|
58
|
+
if (prefix.error || prefix.status !== 0) return null;
|
|
59
|
+
const base = String(prefix.stdout || '').trim();
|
|
60
|
+
return process.platform === 'win32'
|
|
61
|
+
? path.join(base, 'remcp.cmd')
|
|
62
|
+
: path.join(base, 'bin', 'remcp');
|
|
63
|
+
}
|
|
64
|
+
|
|
38
65
|
function jitter(ms) {
|
|
39
66
|
return Math.round(ms * (0.75 + Math.random() * 0.5));
|
|
40
67
|
}
|
|
@@ -220,6 +247,7 @@ export async function runAgent(options) {
|
|
|
220
247
|
send({ type: 'metrics', metrics: deviceMetrics({ reconnects, pendingRequests, runtimeVersion, runtimeRestarts, runtimeDown }) });
|
|
221
248
|
reportInstallOnce();
|
|
222
249
|
flushTelemetry();
|
|
250
|
+
void checkForUpdate();
|
|
223
251
|
});
|
|
224
252
|
ws.on('message', raw => {
|
|
225
253
|
let message;
|
|
@@ -241,6 +269,38 @@ export async function runAgent(options) {
|
|
|
241
269
|
ws.on('error', error => console.error(`ReMCP relay: ${error.message}`));
|
|
242
270
|
}
|
|
243
271
|
|
|
272
|
+
// Auto-update: the server publishes the versions an agent should be running. A newer
|
|
273
|
+
// release is installed in the background and the service restart picks it up; a failed
|
|
274
|
+
// or skipped update leaves the current version running, so an old agent keeps working.
|
|
275
|
+
async function checkForUpdate() {
|
|
276
|
+
if (options.autoUpdate === false) return;
|
|
277
|
+
try {
|
|
278
|
+
const response = await fetch(`${serverUrl}/api/agent/version`, { signal: AbortSignal.timeout(UPDATE_CHECK_TIMEOUT_MS) });
|
|
279
|
+
if (!response.ok) return;
|
|
280
|
+
const advertised = await response.json();
|
|
281
|
+
const minimum = advertised.minimum;
|
|
282
|
+
if (minimum && isNewer(minimum, VERSION)) {
|
|
283
|
+
console.error(`ReMCP ${VERSION} is older than the minimum supported agent ${minimum}; update with: remcp update`);
|
|
284
|
+
}
|
|
285
|
+
if (!isNewer(advertised.cli, VERSION)) return;
|
|
286
|
+
queueEvent({ event: 'agent_update', at: Date.now(), reason: String(advertised.cli).slice(0, 32), success: true });
|
|
287
|
+
const cli = globalCliEntry();
|
|
288
|
+
if (!cli || !existsSync(cli)) {
|
|
289
|
+
console.error(`ReMCP ${advertised.cli} is available; run: remcp update`);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
console.log(`Updating ReMCP to ${advertised.cli}${advertised.runtime ? ` with ${advertised.runtime}` : ''}…`);
|
|
293
|
+
const child = spawn(process.execPath, [cli, 'update', ...(advertised.runtime ? ['--runtime', advertised.runtime] : [])], {
|
|
294
|
+
detached: true,
|
|
295
|
+
stdio: 'ignore',
|
|
296
|
+
env: { ...process.env },
|
|
297
|
+
});
|
|
298
|
+
child.unref();
|
|
299
|
+
} catch {
|
|
300
|
+
// Offline, DNS failure, older server without the endpoint: keep running as-is.
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
244
304
|
telemetryTimer = setInterval(() => {
|
|
245
305
|
if (activeSocket?.readyState === 1) {
|
|
246
306
|
send({ type: 'metrics', metrics: deviceMetrics({ reconnects, pendingRequests, runtimeVersion, runtimeRestarts, runtimeDown, queueDepth: telemetryQueue.length }) });
|
|
@@ -250,12 +310,15 @@ export async function runAgent(options) {
|
|
|
250
310
|
telemetryTimer.unref?.();
|
|
251
311
|
const telemetryFlushTimer = setInterval(flushTelemetry, TELEMETRY_SEND_INTERVAL_MS);
|
|
252
312
|
telemetryFlushTimer.unref?.();
|
|
313
|
+
const updateTimer = setInterval(() => void checkForUpdate(), UPDATE_CHECK_INTERVAL_MS);
|
|
314
|
+
updateTimer.unref?.();
|
|
253
315
|
|
|
254
316
|
async function stop() {
|
|
255
317
|
if (stopping) return;
|
|
256
318
|
stopping = true;
|
|
257
319
|
if (telemetryTimer) clearInterval(telemetryTimer);
|
|
258
320
|
clearInterval(telemetryFlushTimer);
|
|
321
|
+
clearInterval(updateTimer);
|
|
259
322
|
try { activeSocket?.close(); } catch {}
|
|
260
323
|
try { await mcp?.close(); } catch {}
|
|
261
324
|
try { await transport?.close(); } catch {}
|
package/src/cli.mjs
CHANGED
|
@@ -272,6 +272,7 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
272
272
|
const telemetry = telemetryState();
|
|
273
273
|
await runAgent({
|
|
274
274
|
...cfg,
|
|
275
|
+
autoUpdate: cfg.autoUpdate !== false,
|
|
275
276
|
telemetryEnabled: telemetry.enabled,
|
|
276
277
|
installReported: telemetry.installReported,
|
|
277
278
|
installSpec: `${PACKAGE_NAME}@${VERSION}`,
|
|
@@ -280,6 +281,18 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
280
281
|
return;
|
|
281
282
|
}
|
|
282
283
|
|
|
284
|
+
if (command === 'auto-update') {
|
|
285
|
+
const action = String(positional[0] || 'status').toLowerCase();
|
|
286
|
+
const cfg = loadConfig(false) || {};
|
|
287
|
+
if (action === 'status') {
|
|
288
|
+
console.log(JSON.stringify({ autoUpdate: cfg.autoUpdate !== false, checkIntervalHours: 6 }, null, 2));
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (action === 'on' || action === 'enable') { saveConfig({ ...cfg, autoUpdate: true }); console.log('Auto-update enabled.'); return; }
|
|
292
|
+
if (action === 'off' || action === 'disable') { saveConfig({ ...cfg, autoUpdate: false }); console.log('Auto-update disabled. Run `remcp update` yourself when you want a new version.'); return; }
|
|
293
|
+
throw new Error('Usage: remcp auto-update [status|on|off]');
|
|
294
|
+
}
|
|
295
|
+
|
|
283
296
|
if (command === 'telemetry') {
|
|
284
297
|
const action = String(positional[0] || 'status').toLowerCase();
|
|
285
298
|
if (action === 'status') {
|
|
@@ -315,8 +328,20 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
315
328
|
|
|
316
329
|
if (command === 'update') {
|
|
317
330
|
const cfg = loadConfig();
|
|
318
|
-
|
|
319
|
-
|
|
331
|
+
// The server advertises which runtime version it expects; an agent that is updating
|
|
332
|
+
// itself passes it through so client and runtime move together.
|
|
333
|
+
const requested = typeof flags.runtime === 'string' ? flags.runtime.trim() : '';
|
|
334
|
+
if (requested && (!/^@?[a-z0-9._-]+(\/[a-z0-9._-]+)?@\S+$/i.test(requested) || requested.includes(' '))) {
|
|
335
|
+
throw new Error('--runtime must look like @scope/package@1.2.3');
|
|
336
|
+
}
|
|
337
|
+
const runtimeSpec = requested || cfg.runtime.packageSpec;
|
|
338
|
+
if (flags.check) {
|
|
339
|
+
console.log(JSON.stringify({ current: VERSION, runtime: cfg.runtime.packageSpec, available: `${PACKAGE_NAME}@latest` }, null, 2));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
console.log(`Updating ReMCP to the latest published version (${runtimeSpec})…`);
|
|
343
|
+
npmGlobalInstall(`${PACKAGE_NAME}@latest`, runtimeSpec);
|
|
344
|
+
if (requested) saveConfig({ ...cfg, runtime: { ...cfg.runtime, packageSpec: requested } });
|
|
320
345
|
restartPersistentServiceIfInstalled();
|
|
321
346
|
console.log('ReMCP updated. Run `remcp --version` or `remcp status` to verify.');
|
|
322
347
|
return;
|