@hcu-lab.me/mcp-device 1.0.4 → 1.0.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/README.md +6 -0
- package/dist/config-manager.js +55 -18
- package/dist/device/device-state.d.ts +2 -0
- package/dist/device/device-state.js +3 -1
- package/dist/device/device.js +65 -0
- package/dist/device/execution-engine.d.ts +1 -0
- package/dist/device/execution-engine.js +7 -1
- package/dist/device/gateway-channel.d.ts +14 -0
- package/dist/device/gateway-channel.js +120 -3
- package/dist/device/gateway-identity.d.ts +1 -0
- package/dist/device/gateway-identity.js +4 -0
- package/dist/device/linux-service.d.ts +2 -0
- package/dist/device/linux-service.js +2 -2
- package/dist/device/runtime-owner.d.ts +1 -0
- package/dist/device/runtime-owner.js +4 -4
- package/dist/device/self-update.d.ts +115 -0
- package/dist/device/self-update.js +685 -0
- package/dist/device/update-helper.cjs +343 -0
- package/dist/handlers/filesystem-handlers.js +3 -0
- package/dist/index.js +7 -3
- package/dist/npm-scripts/remote.d.ts +5 -0
- package/dist/npm-scripts/remote.js +50 -1
- package/dist/server.js +76 -70
- package/dist/terminal-manager.js +40 -0
- package/dist/utils/capture.js +1 -1
- package/dist/utils/files/factory.js +20 -0
- package/dist/utils/files/text.d.ts +1 -0
- package/dist/utils/files/text.js +78 -34
- package/dist/utils/trackTools.js +23 -2
- package/dist/utils/usageTracker.js +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const fsp = fs.promises;
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
const net = require('net');
|
|
8
|
+
const { execFile } = require('child_process');
|
|
9
|
+
|
|
10
|
+
const statePath = process.argv[2];
|
|
11
|
+
const HANDOFF_WAIT_MS = 120000;
|
|
12
|
+
const PID_WAIT_MS = 30000;
|
|
13
|
+
const PACKAGE_UNLOCK_WAIT_MS = 20000;
|
|
14
|
+
|
|
15
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
16
|
+
|
|
17
|
+
function run(file, args) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
execFile(file, args, { windowsHide: true, maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
20
|
+
if (error) {
|
|
21
|
+
error.stdout = stdout;
|
|
22
|
+
error.stderr = stderr;
|
|
23
|
+
reject(error);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
resolve({ stdout: String(stdout || ''), stderr: String(stderr || '') });
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function exists(filePath) {
|
|
32
|
+
try { await fsp.access(filePath); return true; } catch { return false; }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function readState() {
|
|
36
|
+
return JSON.parse(await fsp.readFile(statePath, 'utf8'));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function writeState(patch) {
|
|
40
|
+
const current = await readState();
|
|
41
|
+
const next = Object.assign({}, current, patch, { updatedAt: Date.now() });
|
|
42
|
+
const tmp =
|
|
43
|
+
statePath + '.' + process.pid + '.' + Date.now() + '.' +
|
|
44
|
+
crypto.randomBytes(6).toString('hex') + '.tmp';
|
|
45
|
+
await fsp.writeFile(tmp, JSON.stringify(next, null, 2), { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
46
|
+
await fsp.rename(tmp, statePath);
|
|
47
|
+
try { await fsp.chmod(statePath, 0o600); } catch {}
|
|
48
|
+
return next;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isAlive(pid) {
|
|
52
|
+
if (!pid || pid < 1) return false;
|
|
53
|
+
try {
|
|
54
|
+
process.kill(pid, 0);
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function waitForFile(filePath, timeoutMs) {
|
|
62
|
+
const deadline = Date.now() + timeoutMs;
|
|
63
|
+
while (Date.now() < deadline) {
|
|
64
|
+
if (await exists(filePath)) return true;
|
|
65
|
+
await sleep(100);
|
|
66
|
+
}
|
|
67
|
+
return await exists(filePath);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function waitForPids(pids, timeoutMs) {
|
|
71
|
+
const filtered = pids.filter(pid => Number.isInteger(pid) && pid > 0);
|
|
72
|
+
const deadline = Date.now() + timeoutMs;
|
|
73
|
+
while (Date.now() < deadline) {
|
|
74
|
+
if (filtered.every(pid => !isAlive(pid))) return true;
|
|
75
|
+
await sleep(100);
|
|
76
|
+
}
|
|
77
|
+
return filtered.every(pid => !isAlive(pid));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function stopManager(state) {
|
|
81
|
+
if (state.managerKind === 'windows-task') {
|
|
82
|
+
// The update helper runs in a separate task, so ending the original device
|
|
83
|
+
// task cannot terminate the helper itself. "not running" is harmless.
|
|
84
|
+
await run(state.schtasksPath, ['/End', '/TN', state.windowsTaskName]).catch(() => {});
|
|
85
|
+
} else if (state.managerKind === 'systemd') {
|
|
86
|
+
await run(state.systemctlPath, ['--user', 'stop', state.systemdUnit]);
|
|
87
|
+
} else if (state.managerKind === 'pm2') {
|
|
88
|
+
await run(state.pm2Path, ['stop', state.pm2Name]);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function startWindowsRunner(state) {
|
|
93
|
+
if (state.managerKind === 'windows-task') {
|
|
94
|
+
await run(state.schtasksPath, ['/Run', '/TN', state.windowsTaskName]);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const escapePs = value => "'" + String(value).replace(/'/g, "''") + "'";
|
|
99
|
+
const command =
|
|
100
|
+
'Start-Process -FilePath ' + escapePs(state.powershellPath) +
|
|
101
|
+
" -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-WindowStyle','Hidden','-File'," +
|
|
102
|
+
escapePs(state.windowsRunnerPath) + ') -WindowStyle Hidden';
|
|
103
|
+
await run(state.powershellPath, ['-NoProfile', '-NonInteractive', '-Command', command]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function startManager(state) {
|
|
107
|
+
if (String(state.managerKind).startsWith('windows-')) {
|
|
108
|
+
await startWindowsRunner(state);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (state.managerKind === 'systemd') {
|
|
112
|
+
await run(state.systemctlPath, ['--user', 'start', state.systemdUnit]);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (state.managerKind === 'pm2') {
|
|
116
|
+
await run(state.pm2Path, ['restart', state.pm2Name]);
|
|
117
|
+
await run(state.pm2Path, ['save']);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
throw new Error('Unsupported manager kind: ' + state.managerKind);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function renameWithRetry(source, target, timeoutMs) {
|
|
124
|
+
const deadline = Date.now() + timeoutMs;
|
|
125
|
+
let lastError = null;
|
|
126
|
+
while (Date.now() < deadline) {
|
|
127
|
+
try {
|
|
128
|
+
await fsp.rename(source, target);
|
|
129
|
+
return;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
lastError = error;
|
|
132
|
+
const code = String(error && error.code || '');
|
|
133
|
+
if (!['EBUSY', 'EPERM', 'EACCES', 'ENOTEMPTY'].includes(code)) throw error;
|
|
134
|
+
await sleep(150);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
throw lastError || new Error('Timed out waiting for package directory to unlock.');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function packageVersion(packageRoot) {
|
|
141
|
+
try {
|
|
142
|
+
const manifest = JSON.parse(await fsp.readFile(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
143
|
+
return String(manifest.version || '');
|
|
144
|
+
} catch {
|
|
145
|
+
return '';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function assertRuntimeOwnerReleased(state) {
|
|
150
|
+
const controlPath = String(state.runtimeOwnerControlPath || '');
|
|
151
|
+
if (!controlPath) {
|
|
152
|
+
const failure = new Error('Runtime owner control path is missing from update state.');
|
|
153
|
+
failure.code = 'DEVICE_UPDATE_RUNTIME_OWNER_UNVERIFIED';
|
|
154
|
+
throw failure;
|
|
155
|
+
}
|
|
156
|
+
let control;
|
|
157
|
+
try {
|
|
158
|
+
control = JSON.parse(await fsp.readFile(controlPath, 'utf8'));
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (error && error.code === 'ENOENT') return;
|
|
161
|
+
const failure = new Error('Unable to verify MCP Device runtime ownership release.');
|
|
162
|
+
failure.code = 'DEVICE_UPDATE_RUNTIME_OWNER_UNVERIFIED';
|
|
163
|
+
throw failure;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const endpoint = String(control && control.endpoint || '');
|
|
167
|
+
if (!endpoint) {
|
|
168
|
+
const failure = new Error('Runtime owner control state has no endpoint; refusing package replacement.');
|
|
169
|
+
failure.code = 'DEVICE_UPDATE_RUNTIME_OWNER_UNVERIFIED';
|
|
170
|
+
throw failure;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const active = await new Promise(resolve => {
|
|
174
|
+
const socket = net.createConnection(endpoint);
|
|
175
|
+
let data = '';
|
|
176
|
+
let settled = false;
|
|
177
|
+
let connected = false;
|
|
178
|
+
const finish = value => {
|
|
179
|
+
if (settled) return;
|
|
180
|
+
settled = true;
|
|
181
|
+
socket.destroy();
|
|
182
|
+
resolve(value);
|
|
183
|
+
};
|
|
184
|
+
socket.setEncoding('utf8');
|
|
185
|
+
socket.setTimeout(800, () => finish(connected));
|
|
186
|
+
socket.once('error', () => finish(false));
|
|
187
|
+
socket.on('data', chunk => { data += chunk; });
|
|
188
|
+
socket.once('end', () => {
|
|
189
|
+
try {
|
|
190
|
+
const parsed = JSON.parse(data);
|
|
191
|
+
finish(parsed && parsed.ok === true && parsed.status && parsed.status.product === 'MCP Device');
|
|
192
|
+
} catch {
|
|
193
|
+
// A live endpoint with an unreadable response is not safe to replace.
|
|
194
|
+
finish(true);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
socket.once('connect', () => {
|
|
198
|
+
connected = true;
|
|
199
|
+
socket.write(JSON.stringify({ action: 'status' }) + '\n');
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (active) {
|
|
204
|
+
const failure = new Error('A live MCP Device runtime owner still exists; refusing package replacement.');
|
|
205
|
+
failure.code = 'DEVICE_UPDATE_RUNTIME_OWNER_ACTIVE';
|
|
206
|
+
throw failure;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// A stale control file is harmless. Do not delete it here: a new owner could
|
|
210
|
+
// have acquired the endpoint between checks, and RuntimeOwner owns cleanup.
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function restoreRollback(state) {
|
|
214
|
+
if (await exists(state.rollbackPath)) {
|
|
215
|
+
await fsp.rm(state.packageRoot, { recursive: true, force: true }).catch(() => {});
|
|
216
|
+
await fsp.rename(state.rollbackPath, state.packageRoot);
|
|
217
|
+
} else {
|
|
218
|
+
const current = await packageVersion(state.packageRoot);
|
|
219
|
+
if (current === state.fromVersion) return;
|
|
220
|
+
if (!await exists(state.rollbackSnapshot)) {
|
|
221
|
+
throw new Error('Offline rollback package is unavailable.');
|
|
222
|
+
}
|
|
223
|
+
await fsp.rm(state.packageRoot, { recursive: true, force: true }).catch(() => {});
|
|
224
|
+
await fsp.cp(state.rollbackSnapshot, state.packageRoot, {
|
|
225
|
+
recursive: true,
|
|
226
|
+
force: true,
|
|
227
|
+
preserveTimestamps: true
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const restored = await packageVersion(state.packageRoot);
|
|
232
|
+
if (restored !== state.fromVersion) {
|
|
233
|
+
throw new Error('Rollback package verification failed: expected ' + state.fromVersion + ', got ' + (restored || 'unknown') + '.');
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function cleanupWindowsUpdateTask(state) {
|
|
238
|
+
if (!state.windowsUpdateTaskName || !state.schtasksPath) return;
|
|
239
|
+
await run(state.schtasksPath, ['/Delete', '/F', '/TN', state.windowsUpdateTaskName]).catch(() => {});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function main() {
|
|
243
|
+
let state = await readState();
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
if (!await waitForFile(state.handoffPath, HANDOFF_WAIT_MS)) {
|
|
247
|
+
const error = new Error('Timed out waiting for MCP Device update handoff.');
|
|
248
|
+
error.code = 'DEVICE_UPDATE_HANDOFF_TIMEOUT';
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
state = await writeState({ state: 'quiescing' });
|
|
253
|
+
await stopManager(state);
|
|
254
|
+
|
|
255
|
+
if (!await waitForPids([state.parentPid, state.childPid], PID_WAIT_MS)) {
|
|
256
|
+
const error = new Error('MCP Device parent/child did not exit before update.');
|
|
257
|
+
error.code = 'DEVICE_UPDATE_RUNTIME_STILL_ALIVE';
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
await assertRuntimeOwnerReleased(state);
|
|
262
|
+
|
|
263
|
+
if (await exists(state.rollbackPath)) {
|
|
264
|
+
const error = new Error('Rollback path already exists; refusing to overwrite recovery evidence.');
|
|
265
|
+
error.code = 'DEVICE_UPDATE_ROLLBACK_PRESENT';
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
await renameWithRetry(state.packageRoot, state.rollbackPath, PACKAGE_UNLOCK_WAIT_MS);
|
|
270
|
+
state = await writeState({ state: 'installing' });
|
|
271
|
+
|
|
272
|
+
await run(state.nodePath, [
|
|
273
|
+
state.npmCliPath,
|
|
274
|
+
'install',
|
|
275
|
+
'-g',
|
|
276
|
+
state.targetTarball,
|
|
277
|
+
'--offline',
|
|
278
|
+
'--cache', state.cacheDir,
|
|
279
|
+
'--prefix', state.globalPrefix,
|
|
280
|
+
'--no-audit',
|
|
281
|
+
'--no-fund'
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
const actual = await packageVersion(state.packageRoot);
|
|
285
|
+
if (actual !== state.targetVersion) {
|
|
286
|
+
const error = new Error('Installed target verification failed: expected ' + state.targetVersion + ', got ' + (actual || 'unknown') + '.');
|
|
287
|
+
error.code = 'DEVICE_UPDATE_VERIFY_FAILED';
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
state = await writeState({
|
|
292
|
+
state: 'installed_waiting_reconnect',
|
|
293
|
+
completedAt: Date.now(),
|
|
294
|
+
errorCode: null,
|
|
295
|
+
message: null
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await startManager(state);
|
|
299
|
+
await cleanupWindowsUpdateTask(state);
|
|
300
|
+
process.exit(0);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
const code = String(error && error.code || 'DEVICE_UPDATE_FAILED').slice(0, 64);
|
|
303
|
+
const message = String(error && error.message || error).slice(0, 240);
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
state = await writeState({ state: 'rollback', errorCode: code, message });
|
|
307
|
+
await restoreRollback(state);
|
|
308
|
+
state = await writeState({
|
|
309
|
+
state: 'failed',
|
|
310
|
+
completedAt: Date.now(),
|
|
311
|
+
errorCode: code,
|
|
312
|
+
message
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Never create a second runtime while any tracked process or owner is
|
|
316
|
+
// still alive. If quiescence failed, preserve bounded failure evidence
|
|
317
|
+
// and leave the original runtime as the only authority.
|
|
318
|
+
const trackedRuntimeAlive = [state.parentPid, state.childPid]
|
|
319
|
+
.filter(pid => Number.isInteger(pid) && pid > 0)
|
|
320
|
+
.some(isAlive);
|
|
321
|
+
if (!trackedRuntimeAlive) {
|
|
322
|
+
try {
|
|
323
|
+
await assertRuntimeOwnerReleased(state);
|
|
324
|
+
await startManager(state);
|
|
325
|
+
} catch {
|
|
326
|
+
// Failure state remains durable for the next safe/manual recovery.
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} catch (rollbackError) {
|
|
330
|
+
await writeState({
|
|
331
|
+
state: 'failed',
|
|
332
|
+
completedAt: Date.now(),
|
|
333
|
+
errorCode: 'DEVICE_UPDATE_ROLLBACK_FAILED',
|
|
334
|
+
message: String(rollbackError && rollbackError.message || rollbackError).slice(0, 240)
|
|
335
|
+
}).catch(() => {});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
await cleanupWindowsUpdateTask(state).catch(() => {});
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
main().catch(() => process.exit(1));
|
|
@@ -53,6 +53,9 @@ export async function handleReadFile(args) {
|
|
|
53
53
|
}
|
|
54
54
|
const readFileOperation = async () => {
|
|
55
55
|
const parsed = ReadFileArgsSchema.parse(args);
|
|
56
|
+
if (process.env.MCP_DEVICE_REMOTE === 'true' && parsed.isUrl) {
|
|
57
|
+
return createErrorResponse('REMOTE_URL_READ_DISABLED: URL reads are disabled for MCP Device remote execution.');
|
|
58
|
+
}
|
|
56
59
|
// Get the configuration for file read limits
|
|
57
60
|
const config = await configManager.getConfig();
|
|
58
61
|
if (!config) {
|
package/dist/index.js
CHANGED
|
@@ -38,9 +38,13 @@ async function runServer() {
|
|
|
38
38
|
deferLog('info', 'Loading configuration...');
|
|
39
39
|
await configManager.loadConfig();
|
|
40
40
|
deferLog('info', 'Configuration loaded successfully');
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
// Remote MCP Device children must not depend on legacy host
|
|
42
|
+
// feature flags or cached A/B state. Their execution surface is fixed by
|
|
43
|
+
// GatewayToolAdapter and remote-mode behavior must be deterministic.
|
|
44
|
+
if (process.env.MCP_DEVICE_REMOTE !== 'true') {
|
|
45
|
+
deferLog('info', 'Initializing feature flags...');
|
|
46
|
+
await featureFlagManager.initialize();
|
|
47
|
+
}
|
|
44
48
|
}
|
|
45
49
|
catch (configError) {
|
|
46
50
|
deferLog('error', `Failed to load configuration: ${configError instanceof Error ? configError.message : String(configError)}`);
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { GatewayDeviceIdentity } from '../device/gateway-identity.js';
|
|
1
2
|
import { GatewayDeviceStatusStore } from '../device/device-status.js';
|
|
2
3
|
export declare function applyServiceCommandStatus(store: GatewayDeviceStatusStore, command: 'install' | 'start' | 'stop' | 'uninstall'): Promise<void>;
|
|
4
|
+
export declare function resolvePairingIdentity(identity: GatewayDeviceIdentity, options?: {
|
|
5
|
+
forcePair?: boolean;
|
|
6
|
+
probeCurrentIdentity?: () => Promise<void>;
|
|
7
|
+
}): Promise<import("../device/gateway-identity.js").GatewayIdentityRecord>;
|
|
3
8
|
export declare function runRemote(): Promise<void>;
|
|
@@ -111,13 +111,32 @@ async function withBootstrapOwner(action, allowTakeover = true) {
|
|
|
111
111
|
await owner.release();
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
|
+
export async function resolvePairingIdentity(identity, options = {}) {
|
|
115
|
+
const current = await identity.loadOrCreate();
|
|
116
|
+
if (!options.forcePair || deviceIdentityNeedsPairing(current))
|
|
117
|
+
return current;
|
|
118
|
+
if (typeof options.probeCurrentIdentity !== 'function') {
|
|
119
|
+
throw new Error('Current device identity probe is required before forced pairing.');
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
await options.probeCurrentIdentity();
|
|
123
|
+
return current;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (String(error?.code || '') !== 'DEVICE_FORGOTTEN')
|
|
127
|
+
throw error;
|
|
128
|
+
await identity.forget();
|
|
129
|
+
return await identity.loadOrCreate();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
114
132
|
async function ensureLinkedDeviceUnderOwner(forcePair = false) {
|
|
115
133
|
const store = new GatewayDeviceStatusStore();
|
|
116
134
|
const current = await store.load();
|
|
117
135
|
const config = await captureGatewayConfigFromEnvironment(new GatewayDeviceConfigStore(), { fallbackGatewayUrl: current.gatewayUrl });
|
|
118
136
|
const gatewayUrl = String(config.gatewayUrl || '').trim();
|
|
119
137
|
const identity = new GatewayDeviceIdentity();
|
|
120
|
-
|
|
138
|
+
let record = await identity.loadOrCreate();
|
|
139
|
+
const initialDeviceId = record.deviceId;
|
|
121
140
|
await store.update({
|
|
122
141
|
gatewayUrl,
|
|
123
142
|
deviceId: record.deviceId,
|
|
@@ -130,6 +149,36 @@ async function ensureLinkedDeviceUnderOwner(forcePair = false) {
|
|
|
130
149
|
const proxy = createGatewayProxyAgent(config);
|
|
131
150
|
let channel;
|
|
132
151
|
try {
|
|
152
|
+
record = await resolvePairingIdentity(identity, {
|
|
153
|
+
forcePair,
|
|
154
|
+
probeCurrentIdentity: async () => {
|
|
155
|
+
const probe = new GatewayDeviceChannel({
|
|
156
|
+
gatewayUrl,
|
|
157
|
+
identity,
|
|
158
|
+
proxyAgent: proxy.agent,
|
|
159
|
+
securityProtocolFloor: config.securityProtocolFloor,
|
|
160
|
+
appCaPem: config.appCaPem || undefined,
|
|
161
|
+
adapter: controlAdapter(),
|
|
162
|
+
agentVersion: process.env.npm_package_version
|
|
163
|
+
});
|
|
164
|
+
try {
|
|
165
|
+
await probe.start();
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
168
|
+
await probe.stop().catch(() => { });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
if (record.deviceId !== initialDeviceId) {
|
|
173
|
+
await store.update({
|
|
174
|
+
gatewayUrl,
|
|
175
|
+
deviceId: record.deviceId,
|
|
176
|
+
deviceName: os.hostname(),
|
|
177
|
+
identityPresent: true,
|
|
178
|
+
account: { connected: false, label: null },
|
|
179
|
+
connection: { ...current.connection, online: false }
|
|
180
|
+
});
|
|
181
|
+
}
|
|
133
182
|
const pairing = await pairGatewayDevice({ gatewayUrl, identity, proxyAgent: proxy.agent, deviceName: os.hostname() });
|
|
134
183
|
await store.update({
|
|
135
184
|
gatewayUrl,
|
package/dist/server.js
CHANGED
|
@@ -147,7 +147,11 @@ server.setRequestHandler(InitializeRequestSchema, async (request) => {
|
|
|
147
147
|
&& currentClient.name !== 'desktop-commander'
|
|
148
148
|
&& !isRemoteClientContext(currentClient.name)
|
|
149
149
|
&& !global.disableOnboarding;
|
|
150
|
-
if (
|
|
150
|
+
if (isRemoteClientContext(currentClient.name)) {
|
|
151
|
+
// Remote MCP Device children must not mutate local onboarding
|
|
152
|
+
// state merely because they initialized an inherited MCP server.
|
|
153
|
+
}
|
|
154
|
+
else if (isWelcomePageEligibleClient) {
|
|
151
155
|
await handleWelcomePageOnboarding(currentClient.name);
|
|
152
156
|
}
|
|
153
157
|
else {
|
|
@@ -1422,7 +1426,7 @@ async function handleCallToolRequest(request) {
|
|
|
1422
1426
|
'get_recent_tool_calls',
|
|
1423
1427
|
'track_ui_event'
|
|
1424
1428
|
];
|
|
1425
|
-
if (!EXCLUDED_TOOLS.includes(name)) {
|
|
1429
|
+
if (!currentCallIsRemote && process.env.MCP_DEVICE_REMOTE !== 'true' && !EXCLUDED_TOOLS.includes(name)) {
|
|
1426
1430
|
toolHistory.addCall(name, args, result, duration);
|
|
1427
1431
|
}
|
|
1428
1432
|
// Track success or failure based on result
|
|
@@ -1436,77 +1440,79 @@ async function handleCallToolRequest(request) {
|
|
|
1436
1440
|
else {
|
|
1437
1441
|
await usageTracker.trackSuccess(name);
|
|
1438
1442
|
console.log(`[FEEDBACK DEBUG] Tool ${name} succeeded, checking feedback...`);
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
// Check if should prompt for feedback (only on successful operations)
|
|
1474
|
-
const shouldPrompt = await usageTracker.shouldPromptForFeedback();
|
|
1475
|
-
console.log(`[FEEDBACK DEBUG] Should prompt for feedback: ${shouldPrompt}`);
|
|
1476
|
-
if (shouldPrompt) {
|
|
1477
|
-
console.log(`[FEEDBACK DEBUG] Generating feedback message...`);
|
|
1478
|
-
const feedbackResult = await usageTracker.getFeedbackPromptMessage();
|
|
1479
|
-
console.log(`[FEEDBACK DEBUG] Generated variant: ${feedbackResult.variant}`);
|
|
1480
|
-
// Capture feedback prompt injection event
|
|
1481
|
-
const stats = await usageTracker.getStats();
|
|
1482
|
-
await capture('feedback_prompt_injected', {
|
|
1483
|
-
trigger_tool: name,
|
|
1484
|
-
total_calls: stats.totalToolCalls,
|
|
1485
|
-
successful_calls: stats.successfulCalls,
|
|
1486
|
-
failed_calls: stats.failedCalls,
|
|
1487
|
-
days_since_first_use: Math.floor((Date.now() - stats.firstUsed) / (1000 * 60 * 60 * 24)),
|
|
1488
|
-
total_sessions: stats.totalSessions,
|
|
1489
|
-
message_variant: feedbackResult.variant
|
|
1490
|
-
});
|
|
1491
|
-
// Inject feedback instruction for the LLM
|
|
1492
|
-
if (result.content && result.content.length > 0 && result.content[0].type === "text") {
|
|
1493
|
-
const currentContent = result.content[0].text || '';
|
|
1494
|
-
result.content[0].text = `${currentContent}${feedbackResult.message}`;
|
|
1443
|
+
if (!currentCallIsRemote && !isRemoteClientContext(currentClient.name)) {
|
|
1444
|
+
// Check if should show onboarding (before feedback - first-time users are priority)
|
|
1445
|
+
const shouldShowOnboarding = await usageTracker.shouldShowOnboarding();
|
|
1446
|
+
console.log(`[ONBOARDING DEBUG] Should show onboarding: ${shouldShowOnboarding}`);
|
|
1447
|
+
if (shouldShowOnboarding) {
|
|
1448
|
+
console.log(`[ONBOARDING DEBUG] Generating onboarding message...`);
|
|
1449
|
+
const onboardingResult = await usageTracker.getOnboardingMessage();
|
|
1450
|
+
console.log(`[ONBOARDING DEBUG] Generated variant: ${onboardingResult.variant}`);
|
|
1451
|
+
// Capture onboarding prompt injection event
|
|
1452
|
+
const stats = await usageTracker.getStats();
|
|
1453
|
+
await capture('server_onboarding_shown', {
|
|
1454
|
+
trigger_tool: name,
|
|
1455
|
+
total_calls: stats.totalToolCalls,
|
|
1456
|
+
successful_calls: stats.successfulCalls,
|
|
1457
|
+
days_since_first_use: Math.floor((Date.now() - stats.firstUsed) / (1000 * 60 * 60 * 24)),
|
|
1458
|
+
total_sessions: stats.totalSessions,
|
|
1459
|
+
message_variant: onboardingResult.variant
|
|
1460
|
+
});
|
|
1461
|
+
// Inject onboarding message for the LLM
|
|
1462
|
+
if (result.content && result.content.length > 0 && result.content[0].type === "text") {
|
|
1463
|
+
const currentContent = result.content[0].text || '';
|
|
1464
|
+
result.content[0].text = `${currentContent}${onboardingResult.message}`;
|
|
1465
|
+
}
|
|
1466
|
+
else {
|
|
1467
|
+
result.content = [
|
|
1468
|
+
...(result.content || []),
|
|
1469
|
+
{
|
|
1470
|
+
type: "text",
|
|
1471
|
+
text: onboardingResult.message
|
|
1472
|
+
}
|
|
1473
|
+
];
|
|
1474
|
+
}
|
|
1475
|
+
// Mark that we've shown onboarding (to prevent spam)
|
|
1476
|
+
await usageTracker.markOnboardingShown(onboardingResult.variant);
|
|
1495
1477
|
}
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1478
|
+
// Check if should prompt for feedback (only on successful operations)
|
|
1479
|
+
const shouldPrompt = await usageTracker.shouldPromptForFeedback();
|
|
1480
|
+
console.log(`[FEEDBACK DEBUG] Should prompt for feedback: ${shouldPrompt}`);
|
|
1481
|
+
if (shouldPrompt) {
|
|
1482
|
+
console.log(`[FEEDBACK DEBUG] Generating feedback message...`);
|
|
1483
|
+
const feedbackResult = await usageTracker.getFeedbackPromptMessage();
|
|
1484
|
+
console.log(`[FEEDBACK DEBUG] Generated variant: ${feedbackResult.variant}`);
|
|
1485
|
+
// Capture feedback prompt injection event
|
|
1486
|
+
const stats = await usageTracker.getStats();
|
|
1487
|
+
await capture('feedback_prompt_injected', {
|
|
1488
|
+
trigger_tool: name,
|
|
1489
|
+
total_calls: stats.totalToolCalls,
|
|
1490
|
+
successful_calls: stats.successfulCalls,
|
|
1491
|
+
failed_calls: stats.failedCalls,
|
|
1492
|
+
days_since_first_use: Math.floor((Date.now() - stats.firstUsed) / (1000 * 60 * 60 * 24)),
|
|
1493
|
+
total_sessions: stats.totalSessions,
|
|
1494
|
+
message_variant: feedbackResult.variant
|
|
1495
|
+
});
|
|
1496
|
+
// Inject feedback instruction for the LLM
|
|
1497
|
+
if (result.content && result.content.length > 0 && result.content[0].type === "text") {
|
|
1498
|
+
const currentContent = result.content[0].text || '';
|
|
1499
|
+
result.content[0].text = `${currentContent}${feedbackResult.message}`;
|
|
1500
|
+
}
|
|
1501
|
+
else {
|
|
1502
|
+
result.content = [
|
|
1503
|
+
...(result.content || []),
|
|
1504
|
+
{
|
|
1505
|
+
type: "text",
|
|
1506
|
+
text: feedbackResult.message
|
|
1507
|
+
}
|
|
1508
|
+
];
|
|
1509
|
+
}
|
|
1510
|
+
// Mark that we've prompted (to prevent spam)
|
|
1511
|
+
await usageTracker.markFeedbackPrompted();
|
|
1504
1512
|
}
|
|
1505
|
-
//
|
|
1506
|
-
await
|
|
1513
|
+
// Check if should prompt about Docker environment
|
|
1514
|
+
result = await processDockerPrompt(result, name);
|
|
1507
1515
|
}
|
|
1508
|
-
// Check if should prompt about Docker environment
|
|
1509
|
-
result = await processDockerPrompt(result, name);
|
|
1510
1516
|
}
|
|
1511
1517
|
// If the caller sent parameters this tool does not support, Zod silently
|
|
1512
1518
|
// strips them. Prepend a corrective warning so the model knows they were
|