@hcu-lab.me/mcp-device 1.0.5 → 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/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 +63 -4
- package/dist/device/execution-engine.d.ts +1 -0
- package/dist/device/execution-engine.js +7 -1
- package/dist/device/gateway-channel.d.ts +13 -1
- package/dist/device/gateway-channel.js +91 -20
- 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 +107 -12
- package/dist/device/self-update.js +660 -62
- 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
package/dist/terminal-manager.js
CHANGED
|
@@ -205,6 +205,21 @@ export class TerminalManager {
|
|
|
205
205
|
// Spawn the process with appropriate arguments
|
|
206
206
|
const childProcess = spawn(spawnConfig.executable, spawnConfig.args, spawnOptions);
|
|
207
207
|
let output = '';
|
|
208
|
+
// spawn() reports resolution/launch failures asynchronously via 'error'.
|
|
209
|
+
// A listener-less ChildProcess error becomes an uncaughtException and kills
|
|
210
|
+
// the inherited MCP server. Attach before any early-return path and keep
|
|
211
|
+
// the listener for the process lifetime.
|
|
212
|
+
let forwardProcessError = null;
|
|
213
|
+
let pendingProcessError = null;
|
|
214
|
+
childProcess.on('error', (err) => {
|
|
215
|
+
if (forwardProcessError) {
|
|
216
|
+
forwardProcessError(err);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
pendingProcessError = err;
|
|
220
|
+
console.error(`Process error for "${command}": ${err.message}`);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
208
223
|
// Ensure childProcess.pid is defined before proceeding
|
|
209
224
|
if (!childProcess.pid) {
|
|
210
225
|
// Return a consistent error object instead of throwing
|
|
@@ -259,6 +274,31 @@ export class TerminalManager {
|
|
|
259
274
|
}
|
|
260
275
|
resolve(result);
|
|
261
276
|
};
|
|
277
|
+
// Once resolveOnce exists, route runtime/spawn errors into the normal
|
|
278
|
+
// command result path so callers do not hang and the session is retired.
|
|
279
|
+
forwardProcessError = (err) => {
|
|
280
|
+
// A late ChildProcess 'error' can arrive after the caller has already
|
|
281
|
+
// received a live/background session. Do not silently retire that
|
|
282
|
+
// session; the normal 'exit' event remains authoritative for cleanup.
|
|
283
|
+
if (resolved) {
|
|
284
|
+
this.appendToLineBuffer(session, `\nProcess error: ${err.message}\n`);
|
|
285
|
+
console.error(`Process error for "${command}": ${err.message}`);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (childProcess.pid)
|
|
289
|
+
this.sessions.delete(childProcess.pid);
|
|
290
|
+
exitReason = 'process_exit';
|
|
291
|
+
resolveOnce({
|
|
292
|
+
pid: childProcess.pid ?? -1,
|
|
293
|
+
output: output + `\nProcess error: ${err.message}`,
|
|
294
|
+
isBlocked: false
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
// Spawn failures normally arrive on the next tick, but replay any error
|
|
298
|
+
// emitted between spawn() and resolver initialization.
|
|
299
|
+
if (pendingProcessError) {
|
|
300
|
+
forwardProcessError(pendingProcessError);
|
|
301
|
+
}
|
|
262
302
|
childProcess.stdout.on('data', (data) => {
|
|
263
303
|
const text = data.toString();
|
|
264
304
|
const now = Date.now();
|
package/dist/utils/capture.js
CHANGED
|
@@ -443,7 +443,7 @@ const postTelemetryPayload = async (endpoint, payload) => {
|
|
|
443
443
|
export const capture = async (event, properties) => {
|
|
444
444
|
// Tool calls fired programmatically by the widget UIs must produce zero
|
|
445
445
|
// telemetry — drop every event raised while serving one.
|
|
446
|
-
if (isInsideUiOriginCall()) {
|
|
446
|
+
if (isInsideUiOriginCall() || isTelemetryDisabledByEnv()) {
|
|
447
447
|
return;
|
|
448
448
|
}
|
|
449
449
|
void (async () => {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Each handler implements canHandle() which can be sync (extension-based)
|
|
6
6
|
* or async (content-based like BinaryFileHandler using isBinaryFile)
|
|
7
7
|
*/
|
|
8
|
+
import fs from 'fs/promises';
|
|
8
9
|
import { TextFileHandler } from './text.js';
|
|
9
10
|
import { ImageFileHandler } from './image.js';
|
|
10
11
|
import { BinaryFileHandler } from './binary.js';
|
|
@@ -86,6 +87,25 @@ export async function getFileHandler(filePath) {
|
|
|
86
87
|
if (getImageHandler().canHandle(filePath)) {
|
|
87
88
|
return getImageHandler();
|
|
88
89
|
}
|
|
90
|
+
// UTF-16LE text contains many NUL bytes and generic binary sniffers
|
|
91
|
+
// often classify it as binary. A BOM is authoritative, so route it to the
|
|
92
|
+
// text handler before content-based binary detection.
|
|
93
|
+
try {
|
|
94
|
+
const fd = await fs.open(filePath, 'r');
|
|
95
|
+
try {
|
|
96
|
+
const bom = Buffer.alloc(2);
|
|
97
|
+
const { bytesRead } = await fd.read(bom, 0, bom.length, 0);
|
|
98
|
+
if (bytesRead === 2 && bom[0] === 0xff && bom[1] === 0xfe) {
|
|
99
|
+
return getTextHandler();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
await fd.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Preserve existing fallback behavior if the file cannot be sampled.
|
|
108
|
+
}
|
|
89
109
|
// Check Binary (content-based, async via isBinaryFile)
|
|
90
110
|
if (await getBinaryHandler().canHandle(filePath)) {
|
|
91
111
|
return getBinaryHandler();
|
|
@@ -19,6 +19,7 @@ import { FileHandler, ReadOptions, FileResult, FileInfo } from './base.js';
|
|
|
19
19
|
*/
|
|
20
20
|
export declare class TextFileHandler implements FileHandler {
|
|
21
21
|
canHandle(_path: string): boolean;
|
|
22
|
+
private detectTextEncoding;
|
|
22
23
|
read(filePath: string, options?: ReadOptions): Promise<FileResult>;
|
|
23
24
|
write(path: string, content: string, mode?: 'rewrite' | 'append'): Promise<void>;
|
|
24
25
|
getInfo(path: string): Promise<FileInfo>;
|
package/dist/utils/files/text.js
CHANGED
|
@@ -38,6 +38,17 @@ export class TextFileHandler {
|
|
|
38
38
|
// The factory routes binary files to BinaryFileHandler before reaching here
|
|
39
39
|
return true;
|
|
40
40
|
}
|
|
41
|
+
async detectTextEncoding(filePath) {
|
|
42
|
+
const fd = await fs.open(filePath, 'r');
|
|
43
|
+
try {
|
|
44
|
+
const bom = Buffer.alloc(2);
|
|
45
|
+
const { bytesRead } = await fd.read(bom, 0, bom.length, 0);
|
|
46
|
+
return bytesRead === 2 && bom[0] === 0xff && bom[1] === 0xfe ? 'utf16le' : 'utf8';
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
await fd.close();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
41
52
|
async read(filePath, options) {
|
|
42
53
|
const offset = options?.offset ?? 0;
|
|
43
54
|
const length = options?.length ?? 1000; // Default from config
|
|
@@ -69,7 +80,11 @@ export class TextFileHandler {
|
|
|
69
80
|
// For text files that aren't too large, count lines
|
|
70
81
|
if (stats.isFile() && stats.size < FILE_SIZE_LIMITS.LINE_COUNT_LIMIT) {
|
|
71
82
|
try {
|
|
72
|
-
const
|
|
83
|
+
const encoding = await this.detectTextEncoding(path);
|
|
84
|
+
let content = await fs.readFile(path, encoding);
|
|
85
|
+
if (encoding === 'utf16le' && content.charCodeAt(0) === 0xfeff) {
|
|
86
|
+
content = content.slice(1);
|
|
87
|
+
}
|
|
73
88
|
const lineCount = TextFileHandler.countLines(content);
|
|
74
89
|
info.metadata.lineCount = lineCount;
|
|
75
90
|
}
|
|
@@ -100,11 +115,14 @@ export class TextFileHandler {
|
|
|
100
115
|
/**
|
|
101
116
|
* Get file line count (for files under size limit)
|
|
102
117
|
*/
|
|
103
|
-
async getFileLineCount(filePath, signal) {
|
|
118
|
+
async getFileLineCount(filePath, signal, encoding = 'utf8') {
|
|
104
119
|
try {
|
|
105
120
|
const stats = await fs.stat(filePath);
|
|
106
121
|
if (stats.size < FILE_SIZE_LIMITS.LINE_COUNT_LIMIT) {
|
|
107
|
-
|
|
122
|
+
let content = await fs.readFile(filePath, { encoding, signal });
|
|
123
|
+
if (encoding === 'utf16le' && content.charCodeAt(0) === 0xfeff) {
|
|
124
|
+
content = content.slice(1);
|
|
125
|
+
}
|
|
108
126
|
return TextFileHandler.countLines(content);
|
|
109
127
|
}
|
|
110
128
|
}
|
|
@@ -182,7 +200,16 @@ export class TextFileHandler {
|
|
|
182
200
|
async readFileWithSmartPositioning(filePath, offset, length, mimeType, includeStatusMessage = true, signal) {
|
|
183
201
|
const stats = await fs.stat(filePath);
|
|
184
202
|
const fileSize = stats.size;
|
|
185
|
-
const
|
|
203
|
+
const encoding = await this.detectTextEncoding(filePath);
|
|
204
|
+
const totalLines = await this.getFileLineCount(filePath, signal, encoding);
|
|
205
|
+
// UTF-16LE needs character-aware streaming. Skip the BOM and preserve
|
|
206
|
+
// the same line pagination/status semantics as UTF-8.
|
|
207
|
+
if (encoding === 'utf16le') {
|
|
208
|
+
if (offset < 0) {
|
|
209
|
+
return await this.readFromEndWithReadline(filePath, Math.abs(offset), mimeType, includeStatusMessage, totalLines, signal, 'utf16le', 2);
|
|
210
|
+
}
|
|
211
|
+
return await this.readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, totalLines, signal, 'utf16le', 2);
|
|
212
|
+
}
|
|
186
213
|
// For negative offsets (tail behavior), use reverse reading
|
|
187
214
|
if (offset < 0) {
|
|
188
215
|
const requestedLines = Math.abs(offset);
|
|
@@ -252,9 +279,9 @@ export class TextFileHandler {
|
|
|
252
279
|
/**
|
|
253
280
|
* Read from end using readline with circular buffer
|
|
254
281
|
*/
|
|
255
|
-
async readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage = true, fileTotalLines, signal) {
|
|
282
|
+
async readFromEndWithReadline(filePath, requestedLines, mimeType, includeStatusMessage = true, fileTotalLines, signal, encoding, start) {
|
|
256
283
|
const rl = createInterface({
|
|
257
|
-
input: createReadStream(filePath, { signal }),
|
|
284
|
+
input: createReadStream(filePath, { signal, encoding, start }),
|
|
258
285
|
crlfDelay: Infinity
|
|
259
286
|
});
|
|
260
287
|
const buffer = new Array(requestedLines);
|
|
@@ -284,22 +311,28 @@ export class TextFileHandler {
|
|
|
284
311
|
/**
|
|
285
312
|
* Read from start/middle using readline
|
|
286
313
|
*/
|
|
287
|
-
async readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines, signal) {
|
|
314
|
+
async readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines, signal, encoding, start) {
|
|
315
|
+
const stream = createReadStream(filePath, { signal, encoding, start });
|
|
288
316
|
const rl = createInterface({
|
|
289
|
-
input:
|
|
317
|
+
input: stream,
|
|
290
318
|
crlfDelay: Infinity
|
|
291
319
|
});
|
|
292
320
|
const result = [];
|
|
293
321
|
let lineNumber = 0;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
result.
|
|
322
|
+
try {
|
|
323
|
+
for await (const line of rl) {
|
|
324
|
+
if (lineNumber >= offset && result.length < length) {
|
|
325
|
+
result.push(line);
|
|
326
|
+
}
|
|
327
|
+
if (result.length >= length)
|
|
328
|
+
break;
|
|
329
|
+
lineNumber++;
|
|
297
330
|
}
|
|
298
|
-
if (result.length >= length)
|
|
299
|
-
break;
|
|
300
|
-
lineNumber++;
|
|
301
331
|
}
|
|
302
|
-
|
|
332
|
+
finally {
|
|
333
|
+
rl.close();
|
|
334
|
+
stream.destroy();
|
|
335
|
+
}
|
|
303
336
|
if (includeStatusMessage) {
|
|
304
337
|
const statusMessage = this.generateEnhancedStatusMessage(result.length, offset, fileTotalLines, false);
|
|
305
338
|
const content = `${statusMessage}\n\n${result.join('\n')}`;
|
|
@@ -314,20 +347,26 @@ export class TextFileHandler {
|
|
|
314
347
|
* Read from estimated byte position for very large files
|
|
315
348
|
*/
|
|
316
349
|
async readFromEstimatedPosition(filePath, offset, length, mimeType, includeStatusMessage = true, fileTotalLines, signal) {
|
|
317
|
-
// First, do a quick scan to estimate lines per byte
|
|
350
|
+
// First, do a quick scan to estimate lines per byte.
|
|
351
|
+
const sampleStream = createReadStream(filePath, { signal });
|
|
318
352
|
const rl = createInterface({
|
|
319
|
-
input:
|
|
353
|
+
input: sampleStream,
|
|
320
354
|
crlfDelay: Infinity
|
|
321
355
|
});
|
|
322
356
|
let sampleLines = 0;
|
|
323
357
|
let bytesRead = 0;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
358
|
+
try {
|
|
359
|
+
for await (const line of rl) {
|
|
360
|
+
bytesRead += Buffer.byteLength(line, 'utf-8') + 1;
|
|
361
|
+
sampleLines++;
|
|
362
|
+
if (bytesRead >= READ_PERFORMANCE_THRESHOLDS.SAMPLE_SIZE)
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
finally {
|
|
367
|
+
rl.close();
|
|
368
|
+
sampleStream.destroy();
|
|
329
369
|
}
|
|
330
|
-
rl.close();
|
|
331
370
|
if (sampleLines === 0) {
|
|
332
371
|
return await this.readFromStartWithReadline(filePath, offset, length, mimeType, includeStatusMessage, fileTotalLines, signal);
|
|
333
372
|
}
|
|
@@ -345,19 +384,24 @@ export class TextFileHandler {
|
|
|
345
384
|
});
|
|
346
385
|
const result = [];
|
|
347
386
|
let firstLineSkipped = false;
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
firstLineSkipped
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
result.
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
387
|
+
try {
|
|
388
|
+
for await (const line of rl2) {
|
|
389
|
+
if (!firstLineSkipped && startPosition > 0) {
|
|
390
|
+
firstLineSkipped = true;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (result.length < length) {
|
|
394
|
+
result.push(line);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
358
399
|
}
|
|
359
400
|
}
|
|
360
|
-
|
|
401
|
+
finally {
|
|
402
|
+
rl2.close();
|
|
403
|
+
stream.destroy();
|
|
404
|
+
}
|
|
361
405
|
const content = includeStatusMessage
|
|
362
406
|
? `${this.generateEnhancedStatusMessage(result.length, offset, fileTotalLines, false)}\n\n${result.join('\n')}`
|
|
363
407
|
: result.join('\n');
|
package/dist/utils/trackTools.js
CHANGED
|
@@ -13,8 +13,29 @@ export async function trackToolCall(toolName, args) {
|
|
|
13
13
|
try {
|
|
14
14
|
// Get current timestamp
|
|
15
15
|
const timestamp = new Date().toISOString();
|
|
16
|
-
//
|
|
17
|
-
|
|
16
|
+
// Remote MCP Device calls may contain commands, file contents, tokens or
|
|
17
|
+
// other secrets. Persist only bounded metadata in remote mode.
|
|
18
|
+
const remote = process.env.MCP_DEVICE_REMOTE === 'true';
|
|
19
|
+
let suffix = '';
|
|
20
|
+
if (args !== undefined) {
|
|
21
|
+
if (remote) {
|
|
22
|
+
let argBytes = 0;
|
|
23
|
+
let argCount = 0;
|
|
24
|
+
try {
|
|
25
|
+
const serialized = JSON.stringify(args);
|
|
26
|
+
argBytes = Buffer.byteLength(serialized || '', 'utf8');
|
|
27
|
+
argCount = args && typeof args === 'object' && !Array.isArray(args)
|
|
28
|
+
? Object.keys(args).length
|
|
29
|
+
: 1;
|
|
30
|
+
}
|
|
31
|
+
catch { /* metadata-only logging must never affect tool execution */ }
|
|
32
|
+
suffix = `\t| Args: count=${argCount} bytes=${argBytes}`;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
suffix = `\t| Arguments: ${JSON.stringify(args)}`;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const logEntry = `${timestamp} | ${toolName.padEnd(20, ' ')}${suffix}\n`;
|
|
18
39
|
// Check if file exists and get its size
|
|
19
40
|
let fileSize = 0;
|
|
20
41
|
try {
|
|
@@ -52,6 +52,8 @@ class UsageTracker {
|
|
|
52
52
|
* keeps a saturated libuv threadpool from gating tool responses on every call.
|
|
53
53
|
*/
|
|
54
54
|
async saveStats(stats) {
|
|
55
|
+
if (process.env.MCP_DEVICE_REMOTE === 'true')
|
|
56
|
+
return;
|
|
55
57
|
await configManager.setValueNonBlocking('usageStats', stats);
|
|
56
58
|
}
|
|
57
59
|
/**
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "1.0.
|
|
1
|
+
export declare const VERSION = "1.0.6";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '1.0.
|
|
1
|
+
export const VERSION = '1.0.6';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hcu-lab.me/mcp-device",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "MCP Device securely connects a user-owned computer to an authenticated Model Context Protocol gateway.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Hirohimeow",
|
|
@@ -26,14 +26,14 @@
|
|
|
26
26
|
"device:start": "tsx src/device/device.ts",
|
|
27
27
|
"device:start:dev": "nodemon --watch src/device --exec tsx src/device/device.ts",
|
|
28
28
|
"sync-version": "node scripts/sync-version.js",
|
|
29
|
-
"version": "node scripts/sync-version.js && git add
|
|
29
|
+
"version": "node scripts/sync-version.js && git add src/version.ts package-lock.json",
|
|
30
30
|
"release": "node scripts/release.js patch",
|
|
31
31
|
"release:minor": "node scripts/release.js minor",
|
|
32
32
|
"release:major": "node scripts/release.js major",
|
|
33
33
|
"bump": "node scripts/sync-version.js --bump",
|
|
34
34
|
"bump:minor": "node scripts/sync-version.js --bump --minor",
|
|
35
35
|
"bump:major": "node scripts/sync-version.js --bump --major",
|
|
36
|
-
"build": "shx rm -rf dist/remote-device && shx rm -f dist/device/desktop-commander-integration.js dist/device/desktop-commander-integration.d.ts dist/device/windows-dpapi.js dist/device/windows-dpapi.d.ts && tsc && shx chmod +x dist/*.js && shx mkdir -p dist/data && shx cp src/data/onboarding-prompts.json dist/data/ && node scripts/copy-official-ca.cjs && node scripts/build-ui-runtime.cjs",
|
|
36
|
+
"build": "shx rm -rf dist/remote-device && shx rm -f dist/device/desktop-commander-integration.js dist/device/desktop-commander-integration.d.ts dist/device/windows-dpapi.js dist/device/windows-dpapi.d.ts && tsc && shx chmod +x dist/*.js && shx cp src/device/update-helper.cjs dist/device/update-helper.cjs && shx mkdir -p dist/data && shx cp src/data/onboarding-prompts.json dist/data/ && node scripts/copy-official-ca.cjs && node scripts/build-ui-runtime.cjs",
|
|
37
37
|
"watch": "tsc --watch",
|
|
38
38
|
"start": "node dist/mcp-device.js",
|
|
39
39
|
"start:debug": "node --inspect-brk=9229 dist/mcp-device.js",
|