@hcu-lab.me/mcp-device 1.0.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/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/bootstrap.d.ts +1 -0
- package/dist/bootstrap.js +22 -0
- package/dist/command-manager.d.ts +8 -0
- package/dist/command-manager.js +241 -0
- package/dist/config-field-definitions.d.ts +41 -0
- package/dist/config-field-definitions.js +37 -0
- package/dist/config-manager.d.ts +102 -0
- package/dist/config-manager.js +291 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.js +10 -0
- package/dist/custom-stdio.d.ts +51 -0
- package/dist/custom-stdio.js +358 -0
- package/dist/data/official-app-ca.pem +12 -0
- package/dist/data/onboarding-prompts.json +70 -0
- package/dist/device/device-state.d.ts +13 -0
- package/dist/device/device-state.js +100 -0
- package/dist/device/device-status.d.ts +81 -0
- package/dist/device/device-status.js +156 -0
- package/dist/device/device.d.ts +16 -0
- package/dist/device/device.js +190 -0
- package/dist/device/execution-engine.d.ts +144 -0
- package/dist/device/execution-engine.js +195 -0
- package/dist/device/gateway-channel.d.ts +48 -0
- package/dist/device/gateway-channel.js +384 -0
- package/dist/device/gateway-config.d.ts +40 -0
- package/dist/device/gateway-config.js +207 -0
- package/dist/device/gateway-identity.d.ts +35 -0
- package/dist/device/gateway-identity.js +178 -0
- package/dist/device/gateway-pairing.d.ts +25 -0
- package/dist/device/gateway-pairing.js +105 -0
- package/dist/device/gateway-secure-transport.d.ts +25 -0
- package/dist/device/gateway-secure-transport.js +117 -0
- package/dist/device/gateway-tool-adapter.d.ts +17 -0
- package/dist/device/gateway-tool-adapter.js +284 -0
- package/dist/device/gateway-url-policy.d.ts +3 -0
- package/dist/device/gateway-url-policy.js +43 -0
- package/dist/device/linux-service.d.ts +70 -0
- package/dist/device/linux-service.js +268 -0
- package/dist/device/official-trust.d.ts +7 -0
- package/dist/device/official-trust.js +49 -0
- package/dist/device/project-inspection.d.ts +1 -0
- package/dist/device/project-inspection.js +180 -0
- package/dist/device/runtime-owner.d.ts +41 -0
- package/dist/device/runtime-owner.js +177 -0
- package/dist/device/windows-dpapi.d.ts +11 -0
- package/dist/device/windows-dpapi.js +67 -0
- package/dist/device/windows-service.d.ts +56 -0
- package/dist/device/windows-service.js +238 -0
- package/dist/error-handlers.d.ts +7 -0
- package/dist/error-handlers.js +15 -0
- package/dist/handlers/edit-search-handlers.d.ts +6 -0
- package/dist/handlers/edit-search-handlers.js +6 -0
- package/dist/handlers/filesystem-handlers.d.ts +38 -0
- package/dist/handlers/filesystem-handlers.js +440 -0
- package/dist/handlers/history-handlers.d.ts +12 -0
- package/dist/handlers/history-handlers.js +67 -0
- package/dist/handlers/index.d.ts +6 -0
- package/dist/handlers/index.js +7 -0
- package/dist/handlers/process-handlers.d.ts +9 -0
- package/dist/handlers/process-handlers.js +15 -0
- package/dist/handlers/search-handlers.d.ts +17 -0
- package/dist/handlers/search-handlers.js +224 -0
- package/dist/handlers/terminal-handlers.d.ts +21 -0
- package/dist/handlers/terminal-handlers.js +35 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +138 -0
- package/dist/mcp-device.d.ts +2 -0
- package/dist/mcp-device.js +7 -0
- package/dist/npm-scripts/remote.d.ts +3 -0
- package/dist/npm-scripts/remote.js +314 -0
- package/dist/search-manager.d.ts +147 -0
- package/dist/search-manager.js +828 -0
- package/dist/server.d.ts +46 -0
- package/dist/server.js +1563 -0
- package/dist/terminal-manager.d.ts +104 -0
- package/dist/terminal-manager.js +635 -0
- package/dist/tools/config.d.ts +96 -0
- package/dist/tools/config.js +260 -0
- package/dist/tools/edit.d.ts +38 -0
- package/dist/tools/edit.js +435 -0
- package/dist/tools/feedback.d.ts +8 -0
- package/dist/tools/feedback.js +132 -0
- package/dist/tools/filesystem.d.ts +79 -0
- package/dist/tools/filesystem.js +947 -0
- package/dist/tools/fuzzySearch.d.ts +12 -0
- package/dist/tools/fuzzySearch.js +74 -0
- package/dist/tools/fuzzySearchCore.d.ts +52 -0
- package/dist/tools/fuzzySearchCore.js +125 -0
- package/dist/tools/improved-process-tools.d.ts +24 -0
- package/dist/tools/improved-process-tools.js +627 -0
- package/dist/tools/mime-types.d.ts +3 -0
- package/dist/tools/mime-types.js +28 -0
- package/dist/tools/pdf/extract-images.d.ts +34 -0
- package/dist/tools/pdf/extract-images.js +132 -0
- package/dist/tools/pdf/index.d.ts +6 -0
- package/dist/tools/pdf/index.js +3 -0
- package/dist/tools/pdf/lib/pdf2md.d.ts +36 -0
- package/dist/tools/pdf/lib/pdf2md.js +76 -0
- package/dist/tools/pdf/manipulations.d.ts +13 -0
- package/dist/tools/pdf/manipulations.js +99 -0
- package/dist/tools/pdf/markdown.d.ts +26 -0
- package/dist/tools/pdf/markdown.js +275 -0
- package/dist/tools/pdf/utils.d.ts +12 -0
- package/dist/tools/pdf/utils.js +34 -0
- package/dist/tools/process.d.ts +3 -0
- package/dist/tools/process.js +56 -0
- package/dist/tools/prompts.d.ts +28 -0
- package/dist/tools/prompts.js +259 -0
- package/dist/tools/schemas.d.ts +454 -0
- package/dist/tools/schemas.js +242 -0
- package/dist/tools/usage.d.ts +5 -0
- package/dist/tools/usage.js +24 -0
- package/dist/types.d.ts +83 -0
- package/dist/types.js +1 -0
- package/dist/ui/config-editor/config-editor-runtime.js +150 -0
- package/dist/ui/config-editor/index.html +13 -0
- package/dist/ui/config-editor/src/app.d.ts +43 -0
- package/dist/ui/config-editor/src/app.js +831 -0
- package/dist/ui/config-editor/src/array-modal.d.ts +19 -0
- package/dist/ui/config-editor/src/array-modal.js +185 -0
- package/dist/ui/config-editor/src/main.d.ts +1 -0
- package/dist/ui/config-editor/src/main.js +2 -0
- package/dist/ui/config-editor/styles.css +587 -0
- package/dist/ui/contracts.d.ts +14 -0
- package/dist/ui/contracts.js +21 -0
- package/dist/ui/file-preview/index.html +16 -0
- package/dist/ui/file-preview/preview-runtime.js +469 -0
- package/dist/ui/file-preview/shared/preview-file-types.d.ts +5 -0
- package/dist/ui/file-preview/shared/preview-file-types.js +59 -0
- package/dist/ui/file-preview/src/app.d.ts +4 -0
- package/dist/ui/file-preview/src/app.js +680 -0
- package/dist/ui/file-preview/src/components/code-viewer.d.ts +6 -0
- package/dist/ui/file-preview/src/components/code-viewer.js +73 -0
- package/dist/ui/file-preview/src/components/highlighting.d.ts +2 -0
- package/dist/ui/file-preview/src/components/highlighting.js +54 -0
- package/dist/ui/file-preview/src/components/html-renderer.d.ts +5 -0
- package/dist/ui/file-preview/src/components/html-renderer.js +47 -0
- package/dist/ui/file-preview/src/components/markdown-renderer.d.ts +1 -0
- package/dist/ui/file-preview/src/components/markdown-renderer.js +59 -0
- package/dist/ui/file-preview/src/directory-controller.d.ts +8 -0
- package/dist/ui/file-preview/src/directory-controller.js +240 -0
- package/dist/ui/file-preview/src/document-layout.d.ts +20 -0
- package/dist/ui/file-preview/src/document-layout.js +109 -0
- package/dist/ui/file-preview/src/document-outline.d.ts +17 -0
- package/dist/ui/file-preview/src/document-outline.js +97 -0
- package/dist/ui/file-preview/src/document-workspace.d.ts +19 -0
- package/dist/ui/file-preview/src/document-workspace.js +33 -0
- package/dist/ui/file-preview/src/file-type-handlers.d.ts +10 -0
- package/dist/ui/file-preview/src/file-type-handlers.js +109 -0
- package/dist/ui/file-preview/src/host/external-actions.d.ts +8 -0
- package/dist/ui/file-preview/src/host/external-actions.js +55 -0
- package/dist/ui/file-preview/src/host/selection-context.d.ts +9 -0
- package/dist/ui/file-preview/src/host/selection-context.js +106 -0
- package/dist/ui/file-preview/src/image-preview.d.ts +3 -0
- package/dist/ui/file-preview/src/image-preview.js +21 -0
- package/dist/ui/file-preview/src/main.d.ts +1 -0
- package/dist/ui/file-preview/src/main.js +5 -0
- package/dist/ui/file-preview/src/markdown/conflict-dialog.d.ts +40 -0
- package/dist/ui/file-preview/src/markdown/conflict-dialog.js +163 -0
- package/dist/ui/file-preview/src/markdown/controller.d.ts +44 -0
- package/dist/ui/file-preview/src/markdown/controller.js +1043 -0
- package/dist/ui/file-preview/src/markdown/editor.d.ts +131 -0
- package/dist/ui/file-preview/src/markdown/editor.js +1479 -0
- package/dist/ui/file-preview/src/markdown/linking.d.ts +16 -0
- package/dist/ui/file-preview/src/markdown/linking.js +228 -0
- package/dist/ui/file-preview/src/markdown/outline.d.ts +2 -0
- package/dist/ui/file-preview/src/markdown/outline.js +16 -0
- package/dist/ui/file-preview/src/markdown/parser.d.ts +30 -0
- package/dist/ui/file-preview/src/markdown/parser.js +38 -0
- package/dist/ui/file-preview/src/markdown/preview.d.ts +1 -0
- package/dist/ui/file-preview/src/markdown/preview.js +20 -0
- package/dist/ui/file-preview/src/markdown/slugify.d.ts +3 -0
- package/dist/ui/file-preview/src/markdown/slugify.js +31 -0
- package/dist/ui/file-preview/src/markdown/utils.d.ts +1 -0
- package/dist/ui/file-preview/src/markdown/utils.js +15 -0
- package/dist/ui/file-preview/src/model.d.ts +35 -0
- package/dist/ui/file-preview/src/model.js +1 -0
- package/dist/ui/file-preview/src/panel-actions.d.ts +17 -0
- package/dist/ui/file-preview/src/panel-actions.js +182 -0
- package/dist/ui/file-preview/src/path-utils.d.ts +6 -0
- package/dist/ui/file-preview/src/path-utils.js +64 -0
- package/dist/ui/file-preview/src/payload-utils.d.ts +11 -0
- package/dist/ui/file-preview/src/payload-utils.js +119 -0
- package/dist/ui/file-preview/src/types.d.ts +1 -0
- package/dist/ui/file-preview/src/types.js +1 -0
- package/dist/ui/file-preview/styles.css +1631 -0
- package/dist/ui/resources.d.ts +28 -0
- package/dist/ui/resources.js +86 -0
- package/dist/ui/shared/compact-row.d.ts +11 -0
- package/dist/ui/shared/compact-row.js +18 -0
- package/dist/ui/shared/escape-html.d.ts +4 -0
- package/dist/ui/shared/escape-html.js +11 -0
- package/dist/ui/shared/host-context.d.ts +15 -0
- package/dist/ui/shared/host-context.js +51 -0
- package/dist/ui/shared/tool-bridge.d.ts +30 -0
- package/dist/ui/shared/tool-bridge.js +137 -0
- package/dist/ui/shared/tool-shell.d.ts +25 -0
- package/dist/ui/shared/tool-shell.js +107 -0
- package/dist/ui/shared/ui-event-tracker.d.ts +9 -0
- package/dist/ui/shared/ui-event-tracker.js +27 -0
- package/dist/ui/shared/widget-state.d.ts +33 -0
- package/dist/ui/shared/widget-state.js +158 -0
- package/dist/utils/ab-test.d.ts +12 -0
- package/dist/utils/ab-test.js +104 -0
- package/dist/utils/capture.d.ts +35 -0
- package/dist/utils/capture.js +481 -0
- package/dist/utils/dockerPrompt.d.ts +17 -0
- package/dist/utils/dockerPrompt.js +53 -0
- package/dist/utils/feature-flags.d.ts +59 -0
- package/dist/utils/feature-flags.js +215 -0
- package/dist/utils/files/base.d.ts +173 -0
- package/dist/utils/files/base.js +5 -0
- package/dist/utils/files/binary.d.ts +21 -0
- package/dist/utils/files/binary.js +65 -0
- package/dist/utils/files/docx.d.ts +47 -0
- package/dist/utils/files/docx.js +679 -0
- package/dist/utils/files/excel.d.ts +24 -0
- package/dist/utils/files/excel.js +437 -0
- package/dist/utils/files/factory.d.ts +41 -0
- package/dist/utils/files/factory.js +113 -0
- package/dist/utils/files/image.d.ts +21 -0
- package/dist/utils/files/image.js +78 -0
- package/dist/utils/files/index.d.ts +10 -0
- package/dist/utils/files/index.js +13 -0
- package/dist/utils/files/pdf.d.ts +32 -0
- package/dist/utils/files/pdf.js +142 -0
- package/dist/utils/files/text.d.ts +63 -0
- package/dist/utils/files/text.js +370 -0
- package/dist/utils/fuzzySearchLogger.d.ts +30 -0
- package/dist/utils/fuzzySearchLogger.js +126 -0
- package/dist/utils/lineEndingHandler.d.ts +21 -0
- package/dist/utils/lineEndingHandler.js +77 -0
- package/dist/utils/logger.d.ts +32 -0
- package/dist/utils/logger.js +72 -0
- package/dist/utils/mcp-ui-ab-test.d.ts +13 -0
- package/dist/utils/mcp-ui-ab-test.js +62 -0
- package/dist/utils/open-browser.d.ts +9 -0
- package/dist/utils/open-browser.js +42 -0
- package/dist/utils/process-detection.d.ts +23 -0
- package/dist/utils/process-detection.js +150 -0
- package/dist/utils/ripgrep-resolver.d.ts +9 -0
- package/dist/utils/ripgrep-resolver.js +74 -0
- package/dist/utils/system-info.d.ts +71 -0
- package/dist/utils/system-info.js +712 -0
- package/dist/utils/toolHistory.d.ts +86 -0
- package/dist/utils/toolHistory.js +257 -0
- package/dist/utils/trackTools.d.ts +6 -0
- package/dist/utils/trackTools.js +54 -0
- package/dist/utils/unsupportedParams.d.ts +24 -0
- package/dist/utils/unsupportedParams.js +66 -0
- package/dist/utils/usageTracker.d.ts +133 -0
- package/dist/utils/usageTracker.js +477 -0
- package/dist/utils/welcome-onboarding.d.ts +11 -0
- package/dist/utils/welcome-onboarding.js +99 -0
- package/dist/utils/withTimeout.d.ts +28 -0
- package/dist/utils/withTimeout.js +85 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +131 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import tls from 'tls';
|
|
3
|
+
import { Duplex } from 'stream';
|
|
4
|
+
import WebSocket from 'ws';
|
|
5
|
+
export const DEVICE_INNER_TLS_SUBPROTOCOL = 'hcu-device-v2-inner-tls';
|
|
6
|
+
export const DEVICE_MAX_MESSAGE_BYTES = 64 * 1024;
|
|
7
|
+
export class WebSocketDuplex extends Duplex {
|
|
8
|
+
constructor(ws) {
|
|
9
|
+
super();
|
|
10
|
+
this.ws = ws;
|
|
11
|
+
ws.on('message', (data, isBinary) => {
|
|
12
|
+
if (!isBinary) {
|
|
13
|
+
this.destroy(new Error('Inner TLS transport requires binary WebSocket messages.'));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const chunk = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
17
|
+
this.push(chunk);
|
|
18
|
+
});
|
|
19
|
+
ws.once('close', () => this.push(null));
|
|
20
|
+
ws.once('error', error => this.destroy(error));
|
|
21
|
+
}
|
|
22
|
+
_read() { }
|
|
23
|
+
_write(chunk, _encoding, callback) {
|
|
24
|
+
if (this.ws.readyState !== WebSocket.OPEN) {
|
|
25
|
+
callback(new Error('WebSocket is not open.'));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
this.ws.send(Buffer.from(chunk), { binary: true }, callback);
|
|
29
|
+
}
|
|
30
|
+
_destroy(error, callback) {
|
|
31
|
+
try {
|
|
32
|
+
if (this.ws.readyState === WebSocket.OPEN)
|
|
33
|
+
this.ws.close(1000, 'inner transport closed');
|
|
34
|
+
else if (this.ws.readyState === WebSocket.CONNECTING)
|
|
35
|
+
this.ws.terminate();
|
|
36
|
+
}
|
|
37
|
+
catch { }
|
|
38
|
+
callback(error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function createWebSocketDuplex(ws) {
|
|
42
|
+
return new WebSocketDuplex(ws);
|
|
43
|
+
}
|
|
44
|
+
export function createClientInnerTls(ws, options) {
|
|
45
|
+
return tls.connect({
|
|
46
|
+
socket: createWebSocketDuplex(ws),
|
|
47
|
+
ca: options.ca,
|
|
48
|
+
servername: options.servername,
|
|
49
|
+
minVersion: 'TLSv1.3',
|
|
50
|
+
rejectUnauthorized: true
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function exportDeviceAuthKeyingMaterial(socket, deviceId, mode) {
|
|
54
|
+
const context = createHash('sha256')
|
|
55
|
+
.update(`hcu-mcp-device-auth-v2\n${String(deviceId)}\n${String(mode)}`, 'utf8')
|
|
56
|
+
.digest();
|
|
57
|
+
return socket.exportKeyingMaterial(32, 'EXPERIMENTAL-HCU-MCP-DEVICE-AUTH-V2', context);
|
|
58
|
+
}
|
|
59
|
+
export function encodeJsonFrame(message, maxMessageBytes = DEVICE_MAX_MESSAGE_BYTES) {
|
|
60
|
+
const payload = Buffer.from(JSON.stringify(message), 'utf8');
|
|
61
|
+
if (payload.length < 1 || payload.length > maxMessageBytes) {
|
|
62
|
+
throw new Error('Device JSON frame exceeds maximum size.');
|
|
63
|
+
}
|
|
64
|
+
const frame = Buffer.allocUnsafe(4 + payload.length);
|
|
65
|
+
frame.writeUInt32BE(payload.length, 0);
|
|
66
|
+
payload.copy(frame, 4);
|
|
67
|
+
return frame;
|
|
68
|
+
}
|
|
69
|
+
export function createJsonFrameParser(options) {
|
|
70
|
+
const maxMessageBytes = options.maxMessageBytes ?? DEVICE_MAX_MESSAGE_BYTES;
|
|
71
|
+
if (typeof options.onMessage !== 'function')
|
|
72
|
+
throw new Error('onMessage callback is required.');
|
|
73
|
+
const header = Buffer.allocUnsafe(4);
|
|
74
|
+
let headerBytes = 0;
|
|
75
|
+
let expectedLength = null;
|
|
76
|
+
let bodyBytes = 0;
|
|
77
|
+
let bodyParts = [];
|
|
78
|
+
return {
|
|
79
|
+
push(value) {
|
|
80
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
81
|
+
let offset = 0;
|
|
82
|
+
while (offset < chunk.length) {
|
|
83
|
+
if (expectedLength === null) {
|
|
84
|
+
const take = Math.min(4 - headerBytes, chunk.length - offset);
|
|
85
|
+
chunk.copy(header, headerBytes, offset, offset + take);
|
|
86
|
+
headerBytes += take;
|
|
87
|
+
offset += take;
|
|
88
|
+
if (headerBytes < 4)
|
|
89
|
+
continue;
|
|
90
|
+
const length = header.readUInt32BE(0);
|
|
91
|
+
headerBytes = 0;
|
|
92
|
+
if (length < 1 || length > maxMessageBytes)
|
|
93
|
+
throw new Error('Invalid device JSON frame length.');
|
|
94
|
+
expectedLength = length;
|
|
95
|
+
bodyBytes = 0;
|
|
96
|
+
bodyParts = [];
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const remaining = expectedLength - bodyBytes;
|
|
100
|
+
const take = Math.min(remaining, chunk.length - offset);
|
|
101
|
+
bodyParts.push(chunk.subarray(offset, offset + take));
|
|
102
|
+
bodyBytes += take;
|
|
103
|
+
offset += take;
|
|
104
|
+
if (bodyBytes !== expectedLength)
|
|
105
|
+
continue;
|
|
106
|
+
const text = Buffer.concat(bodyParts, expectedLength).toString('utf8');
|
|
107
|
+
expectedLength = null;
|
|
108
|
+
bodyBytes = 0;
|
|
109
|
+
bodyParts = [];
|
|
110
|
+
const message = JSON.parse(text);
|
|
111
|
+
if (!message || typeof message !== 'object')
|
|
112
|
+
throw new Error('Device JSON frame must contain an object.');
|
|
113
|
+
options.onMessage(message);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DesktopCommanderIntegration } from './execution-engine.js';
|
|
2
|
+
export declare const GATEWAY_CAPABILITIES: readonly ["read_text_file", "write_file", "edit_file", "shell_execute", "start_process", "read_process_output", "interact_with_process", "terminate_process", "image_preview", "project_inspect"];
|
|
3
|
+
export interface GatewayToolAdapterOptions {
|
|
4
|
+
allowedRoots?: string[];
|
|
5
|
+
pathValidator?: (requestedPath: string) => Promise<string>;
|
|
6
|
+
}
|
|
7
|
+
export declare class GatewayToolAdapter {
|
|
8
|
+
private desktop;
|
|
9
|
+
private allowedRoots;
|
|
10
|
+
private pathValidator;
|
|
11
|
+
private canonicalRoots?;
|
|
12
|
+
constructor(desktop: DesktopCommanderIntegration, options?: GatewayToolAdapterOptions);
|
|
13
|
+
private guardPath;
|
|
14
|
+
private imagePreview;
|
|
15
|
+
call(tool: string, args?: any): Promise<any>;
|
|
16
|
+
private editFile;
|
|
17
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { exec, execFile } from 'child_process';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import sharp from 'sharp';
|
|
6
|
+
import { commandManager } from '../command-manager.js';
|
|
7
|
+
import { configManager } from '../config-manager.js';
|
|
8
|
+
import { validatePath } from '../tools/filesystem.js';
|
|
9
|
+
import { inspectProjectOnDevice } from './project-inspection.js';
|
|
10
|
+
const execAsync = promisify(exec);
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
const MAX_REMOTE_IMAGE_SOURCE_BYTES = 32 * 1024 * 1024;
|
|
13
|
+
const MAX_REMOTE_IMAGE_PREVIEW_BYTES = 32 * 1024;
|
|
14
|
+
const IMAGE_PREVIEW_ATTEMPTS = [
|
|
15
|
+
{ size: 512, quality: 65 },
|
|
16
|
+
{ size: 384, quality: 55 },
|
|
17
|
+
{ size: 256, quality: 45 },
|
|
18
|
+
{ size: 160, quality: 35 }
|
|
19
|
+
];
|
|
20
|
+
export const GATEWAY_CAPABILITIES = [
|
|
21
|
+
'read_text_file',
|
|
22
|
+
'write_file',
|
|
23
|
+
'edit_file',
|
|
24
|
+
'shell_execute',
|
|
25
|
+
'start_process',
|
|
26
|
+
'read_process_output',
|
|
27
|
+
'interact_with_process',
|
|
28
|
+
'terminate_process',
|
|
29
|
+
'image_preview',
|
|
30
|
+
'project_inspect'
|
|
31
|
+
];
|
|
32
|
+
function configuredGatewayRoots() {
|
|
33
|
+
const raw = String(process.env.MCP_GATEWAY_ALLOWED_ROOTS || '').trim();
|
|
34
|
+
if (!raw)
|
|
35
|
+
return [];
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(raw);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
throw new Error('MCP_GATEWAY_ALLOWED_ROOTS must be a JSON array of absolute paths');
|
|
42
|
+
}
|
|
43
|
+
if (!Array.isArray(parsed))
|
|
44
|
+
throw new Error('MCP_GATEWAY_ALLOWED_ROOTS must be a JSON array of absolute paths');
|
|
45
|
+
return [...new Set(parsed.map(value => String(value).trim()).filter(Boolean))];
|
|
46
|
+
}
|
|
47
|
+
function isWithinRoot(candidate, root) {
|
|
48
|
+
const relative = path.relative(root, candidate);
|
|
49
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
50
|
+
}
|
|
51
|
+
function textFromResult(result) {
|
|
52
|
+
return (result?.content || [])
|
|
53
|
+
.filter((item) => item?.type === 'text')
|
|
54
|
+
.map((item) => String(item.text || ''))
|
|
55
|
+
.join('\n');
|
|
56
|
+
}
|
|
57
|
+
function assertSuccess(result, tool) {
|
|
58
|
+
if (result?.isError)
|
|
59
|
+
throw new Error(textFromResult(result) || `${tool} failed`);
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
function parsePid(result) {
|
|
63
|
+
const match = textFromResult(result).match(/PID\s+(-?\d+)/i);
|
|
64
|
+
const pid = Number(match?.[1]);
|
|
65
|
+
if (!Number.isInteger(pid))
|
|
66
|
+
throw new Error('Desktop Commander did not return a process PID');
|
|
67
|
+
return pid;
|
|
68
|
+
}
|
|
69
|
+
async function runShell(args) {
|
|
70
|
+
const command = String(args.command || '');
|
|
71
|
+
if (!await commandManager.validateCommand(command))
|
|
72
|
+
throw new Error(`Command not allowed: ${command}`);
|
|
73
|
+
const cwd = args.working_directory
|
|
74
|
+
? await validatePath(String(args.working_directory))
|
|
75
|
+
: process.cwd();
|
|
76
|
+
const config = await configManager.getConfig();
|
|
77
|
+
const timeoutMs = Number(args.timeout_ms || 28000);
|
|
78
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 28000) {
|
|
79
|
+
throw new Error('Remote shell_execute timeout_ms must be between 1 and 28000');
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const shell = config.defaultShell || undefined;
|
|
83
|
+
const shellName = shell ? path.basename(shell).toLowerCase() : '';
|
|
84
|
+
const options = {
|
|
85
|
+
cwd,
|
|
86
|
+
timeout: timeoutMs,
|
|
87
|
+
windowsHide: true,
|
|
88
|
+
maxBuffer: 1024 * 1024,
|
|
89
|
+
encoding: 'utf8'
|
|
90
|
+
};
|
|
91
|
+
const result = shellName === 'powershell' || shellName === 'powershell.exe' || shellName === 'pwsh' || shellName === 'pwsh.exe'
|
|
92
|
+
? await execFileAsync(shell, ['-NoProfile', '-NonInteractive', '-Command', command], options)
|
|
93
|
+
: await execAsync(command, { ...options, shell });
|
|
94
|
+
const { stdout, stderr } = result;
|
|
95
|
+
return {
|
|
96
|
+
workingDirectoryResolved: cwd,
|
|
97
|
+
exitCode: 0,
|
|
98
|
+
stdout,
|
|
99
|
+
stderr,
|
|
100
|
+
timedOut: false,
|
|
101
|
+
stdoutTruncated: false,
|
|
102
|
+
stderrTruncated: false,
|
|
103
|
+
stdoutSpillPath: null,
|
|
104
|
+
stderrSpillPath: null
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
const timedOut = error?.killed === true && error?.signal;
|
|
109
|
+
return {
|
|
110
|
+
workingDirectoryResolved: cwd,
|
|
111
|
+
exitCode: timedOut ? 124 : Number.isInteger(error?.code) ? error.code : 1,
|
|
112
|
+
stdout: String(error?.stdout || ''),
|
|
113
|
+
stderr: String(error?.stderr || error?.message || ''),
|
|
114
|
+
timedOut: Boolean(timedOut),
|
|
115
|
+
stdoutTruncated: false,
|
|
116
|
+
stderrTruncated: false,
|
|
117
|
+
stdoutSpillPath: null,
|
|
118
|
+
stderrSpillPath: null
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export class GatewayToolAdapter {
|
|
123
|
+
constructor(desktop, options = {}) {
|
|
124
|
+
this.desktop = desktop;
|
|
125
|
+
this.allowedRoots = options.allowedRoots ?? configuredGatewayRoots();
|
|
126
|
+
this.pathValidator = options.pathValidator ?? validatePath;
|
|
127
|
+
}
|
|
128
|
+
async guardPath(requestedPath) {
|
|
129
|
+
const value = String(requestedPath || '').trim();
|
|
130
|
+
if (!value)
|
|
131
|
+
throw new Error('Remote device path/working_directory is required');
|
|
132
|
+
const candidate = await this.pathValidator(value);
|
|
133
|
+
if (!this.allowedRoots.length)
|
|
134
|
+
return candidate;
|
|
135
|
+
this.canonicalRoots ?? (this.canonicalRoots = Promise.all(this.allowedRoots.map(root => this.pathValidator(root))));
|
|
136
|
+
const roots = await this.canonicalRoots;
|
|
137
|
+
if (!roots.some(root => isWithinRoot(candidate, root))) {
|
|
138
|
+
throw new Error('Remote device path is outside MCP_GATEWAY_ALLOWED_ROOTS');
|
|
139
|
+
}
|
|
140
|
+
return candidate;
|
|
141
|
+
}
|
|
142
|
+
async imagePreview(args) {
|
|
143
|
+
const requestedPath = args.path || args.file || args.sourcePath;
|
|
144
|
+
const filePath = await this.guardPath(requestedPath);
|
|
145
|
+
const stat = await fs.stat(filePath);
|
|
146
|
+
if (!stat.isFile())
|
|
147
|
+
throw new Error('Remote image preview path is not a file');
|
|
148
|
+
const requestedMaxBytes = Number(args.maxBytes ?? 8 * 1024 * 1024);
|
|
149
|
+
const maxBytes = Number.isFinite(requestedMaxBytes)
|
|
150
|
+
? Math.max(1, Math.min(MAX_REMOTE_IMAGE_SOURCE_BYTES, Math.floor(requestedMaxBytes)))
|
|
151
|
+
: 8 * 1024 * 1024;
|
|
152
|
+
if (stat.size > maxBytes)
|
|
153
|
+
throw new Error(`Remote image source exceeds maxBytes (${stat.size} > ${maxBytes})`);
|
|
154
|
+
const includeImage = Boolean(args.embed ?? args.includeImage ?? args.includeData ?? true);
|
|
155
|
+
const source = sharp(filePath, { animated: false, limitInputPixels: 64 * 1024 * 1024 });
|
|
156
|
+
const metadata = await source.metadata();
|
|
157
|
+
const text = {
|
|
158
|
+
ok: true,
|
|
159
|
+
tool: 'image_preview',
|
|
160
|
+
summary: includeImage ? 'Loaded bounded remote image preview.' : 'Loaded remote image metadata.',
|
|
161
|
+
data: {
|
|
162
|
+
path: filePath,
|
|
163
|
+
bytes: stat.size,
|
|
164
|
+
width: metadata.width ?? null,
|
|
165
|
+
height: metadata.height ?? null,
|
|
166
|
+
sourceFormat: metadata.format ?? null,
|
|
167
|
+
mimeType: 'image/webp',
|
|
168
|
+
embedded: includeImage
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
if (!includeImage)
|
|
172
|
+
return { content: [{ type: 'text', text: JSON.stringify(text) }] };
|
|
173
|
+
for (const attempt of IMAGE_PREVIEW_ATTEMPTS) {
|
|
174
|
+
const preview = await sharp(filePath, { animated: false, limitInputPixels: 64 * 1024 * 1024 })
|
|
175
|
+
.rotate()
|
|
176
|
+
.resize({ width: attempt.size, height: attempt.size, fit: 'inside', withoutEnlargement: true })
|
|
177
|
+
.webp({ quality: attempt.quality })
|
|
178
|
+
.toBuffer();
|
|
179
|
+
if (preview.length <= MAX_REMOTE_IMAGE_PREVIEW_BYTES) {
|
|
180
|
+
return {
|
|
181
|
+
content: [
|
|
182
|
+
{ type: 'text', text: JSON.stringify({ ...text, data: { ...text.data, previewBytes: preview.length } }) },
|
|
183
|
+
{ type: 'image', data: preview.toString('base64'), mimeType: 'image/webp' }
|
|
184
|
+
]
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
throw new Error(`REMOTE_IMAGE_TOO_LARGE: unable to fit preview within ${MAX_REMOTE_IMAGE_PREVIEW_BYTES} bytes`);
|
|
189
|
+
}
|
|
190
|
+
async call(tool, args = {}) {
|
|
191
|
+
if (tool === 'read_text_file') {
|
|
192
|
+
if (args.head !== undefined && args.tail !== undefined)
|
|
193
|
+
throw new Error('Use either head or tail, not both');
|
|
194
|
+
const mapped = { path: await this.guardPath(args.path) };
|
|
195
|
+
if (args.head !== undefined) {
|
|
196
|
+
mapped.offset = 0;
|
|
197
|
+
mapped.length = Number(args.head);
|
|
198
|
+
}
|
|
199
|
+
else if (args.tail !== undefined) {
|
|
200
|
+
mapped.offset = -Number(args.tail);
|
|
201
|
+
}
|
|
202
|
+
return assertSuccess(await this.desktop.callClientTool('read_file', mapped), 'read_file');
|
|
203
|
+
}
|
|
204
|
+
if (tool === 'write_file') {
|
|
205
|
+
return assertSuccess(await this.desktop.callClientTool('write_file', {
|
|
206
|
+
path: await this.guardPath(args.path),
|
|
207
|
+
content: args.content,
|
|
208
|
+
mode: 'rewrite'
|
|
209
|
+
}), 'write_file');
|
|
210
|
+
}
|
|
211
|
+
if (tool === 'edit_file')
|
|
212
|
+
return await this.editFile({ ...args, path: await this.guardPath(args.path) });
|
|
213
|
+
if (tool === 'shell_execute')
|
|
214
|
+
return await runShell({ ...args, working_directory: await this.guardPath(args.working_directory) });
|
|
215
|
+
if (tool === 'start_process') {
|
|
216
|
+
const workingDirectory = await this.guardPath(args.working_directory);
|
|
217
|
+
const result = assertSuccess(await this.desktop.callClientTool('start_process', {
|
|
218
|
+
command: args.command,
|
|
219
|
+
timeout_ms: Number(args.timeout_ms || 10000),
|
|
220
|
+
working_directory: workingDirectory
|
|
221
|
+
}), 'start_process');
|
|
222
|
+
return { ...result, pid: parsePid(result), session_id: String(parsePid(result)) };
|
|
223
|
+
}
|
|
224
|
+
if (tool === 'read_process_output') {
|
|
225
|
+
return assertSuccess(await this.desktop.callClientTool('read_process_output', {
|
|
226
|
+
pid: Number(args.session_id),
|
|
227
|
+
offset: args.offset,
|
|
228
|
+
length: args.length,
|
|
229
|
+
timeout_ms: 5000
|
|
230
|
+
}), 'read_process_output');
|
|
231
|
+
}
|
|
232
|
+
if (tool === 'interact_with_process') {
|
|
233
|
+
return assertSuccess(await this.desktop.callClientTool('interact_with_process', {
|
|
234
|
+
pid: Number(args.session_id),
|
|
235
|
+
input: String(args.input ?? ''),
|
|
236
|
+
timeout_ms: Number(args.timeout_ms || 8000)
|
|
237
|
+
}), 'interact_with_process');
|
|
238
|
+
}
|
|
239
|
+
if (tool === 'terminate_process') {
|
|
240
|
+
return assertSuccess(await this.desktop.callClientTool('force_terminate', {
|
|
241
|
+
pid: Number(args.session_id)
|
|
242
|
+
}), 'force_terminate');
|
|
243
|
+
}
|
|
244
|
+
if (tool === 'image_preview')
|
|
245
|
+
return await this.imagePreview(args);
|
|
246
|
+
if (tool === 'project_inspect') {
|
|
247
|
+
return await inspectProjectOnDevice(await this.guardPath(args.path), args);
|
|
248
|
+
}
|
|
249
|
+
throw new Error(`Unsupported gateway capability: ${tool}`);
|
|
250
|
+
}
|
|
251
|
+
async editFile(args) {
|
|
252
|
+
if (typeof args.old_text !== 'string' || args.old_text.length === 0)
|
|
253
|
+
throw new Error('old_text is required');
|
|
254
|
+
if (typeof args.new_text !== 'string')
|
|
255
|
+
throw new Error('new_text is required');
|
|
256
|
+
const expectedReplacements = Number(args.expected_replacements ?? 1);
|
|
257
|
+
if (!Number.isInteger(expectedReplacements) || expectedReplacements < 1) {
|
|
258
|
+
throw new Error('expected_replacements must be a positive integer');
|
|
259
|
+
}
|
|
260
|
+
if (args.dry_run === true) {
|
|
261
|
+
const read = assertSuccess(await this.desktop.callClientTool('read_file', {
|
|
262
|
+
path: args.path,
|
|
263
|
+
offset: 0,
|
|
264
|
+
length: 100000
|
|
265
|
+
}), 'read_file');
|
|
266
|
+
const source = textFromResult(read);
|
|
267
|
+
const count = source.split(args.old_text).length - 1;
|
|
268
|
+
return {
|
|
269
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
270
|
+
ok: count === expectedReplacements,
|
|
271
|
+
dry_run: true,
|
|
272
|
+
expected_replacements: expectedReplacements,
|
|
273
|
+
actual_count: count
|
|
274
|
+
}) }]
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
return assertSuccess(await this.desktop.callClientTool('edit_block', {
|
|
278
|
+
file_path: args.path,
|
|
279
|
+
old_string: args.old_text,
|
|
280
|
+
new_string: args.new_text,
|
|
281
|
+
expected_replacements: expectedReplacements
|
|
282
|
+
}), 'edit_block');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
function isLoopbackHostname(hostname) {
|
|
2
|
+
const host = String(hostname || '').trim().toLowerCase().replace(/^\[|\]$/g, '');
|
|
3
|
+
if (host === 'localhost' || host === '::1')
|
|
4
|
+
return true;
|
|
5
|
+
const parts = host.split('.');
|
|
6
|
+
return parts.length === 4 && parts.every(part => /^\d+$/.test(part)) && Number(parts[0]) === 127 && parts.every(part => Number(part) >= 0 && Number(part) <= 255);
|
|
7
|
+
}
|
|
8
|
+
export function assertSecureGatewayUrl(url) {
|
|
9
|
+
if ((url.protocol === 'http:' || url.protocol === 'ws:') && !isLoopbackHostname(url.hostname)) {
|
|
10
|
+
throw new Error('Plaintext MCP gateway URLs are allowed only on loopback; use https/wss for non-loopback gateways.');
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function gatewaySocketUrl(raw) {
|
|
14
|
+
const url = new URL(raw);
|
|
15
|
+
const configuredProtocol = url.protocol;
|
|
16
|
+
if (url.protocol === 'http:')
|
|
17
|
+
url.protocol = 'ws:';
|
|
18
|
+
if (url.protocol === 'https:')
|
|
19
|
+
url.protocol = 'wss:';
|
|
20
|
+
if (!['ws:', 'wss:'].includes(url.protocol))
|
|
21
|
+
throw new Error('MCP_GATEWAY_URL must use http(s) or ws(s)');
|
|
22
|
+
assertSecureGatewayUrl(url);
|
|
23
|
+
if (configuredProtocol === 'http:' || configuredProtocol === 'https:' || !url.pathname || url.pathname === '/') {
|
|
24
|
+
url.pathname = '/device';
|
|
25
|
+
url.search = '';
|
|
26
|
+
url.hash = '';
|
|
27
|
+
}
|
|
28
|
+
return url.toString();
|
|
29
|
+
}
|
|
30
|
+
export function gatewayHttpBase(raw) {
|
|
31
|
+
const url = new URL(raw);
|
|
32
|
+
if (url.protocol === 'ws:')
|
|
33
|
+
url.protocol = 'http:';
|
|
34
|
+
if (url.protocol === 'wss:')
|
|
35
|
+
url.protocol = 'https:';
|
|
36
|
+
if (!['http:', 'https:'].includes(url.protocol))
|
|
37
|
+
throw new Error('Gateway URL must use http(s) or ws(s).');
|
|
38
|
+
assertSecureGatewayUrl(url);
|
|
39
|
+
url.pathname = '/';
|
|
40
|
+
url.search = '';
|
|
41
|
+
url.hash = '';
|
|
42
|
+
return url.toString().replace(/\/$/, '');
|
|
43
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
type ExecFileResult = {
|
|
2
|
+
stdout: string;
|
|
3
|
+
stderr: string;
|
|
4
|
+
};
|
|
5
|
+
type ExecFileFn = (file: string, args: string[]) => Promise<ExecFileResult>;
|
|
6
|
+
export type LinuxManager = 'systemd' | 'pm2';
|
|
7
|
+
export type LinuxServiceStatus = {
|
|
8
|
+
installed: boolean;
|
|
9
|
+
running: boolean;
|
|
10
|
+
autostart: boolean;
|
|
11
|
+
manager: LinuxManager;
|
|
12
|
+
};
|
|
13
|
+
export declare function chooseLinuxManager({ ask }?: {
|
|
14
|
+
ask?: () => Promise<string>;
|
|
15
|
+
}): Promise<LinuxManager>;
|
|
16
|
+
export declare class SystemdUserDeviceService {
|
|
17
|
+
private platform;
|
|
18
|
+
private execFile;
|
|
19
|
+
readonly unitPath: string;
|
|
20
|
+
private nodePath;
|
|
21
|
+
private entrypoint;
|
|
22
|
+
private pathValue;
|
|
23
|
+
constructor(options?: {
|
|
24
|
+
platform?: NodeJS.Platform | string;
|
|
25
|
+
execFile?: ExecFileFn;
|
|
26
|
+
unitPath?: string;
|
|
27
|
+
nodePath?: string;
|
|
28
|
+
entrypoint?: string;
|
|
29
|
+
pathValue?: string;
|
|
30
|
+
});
|
|
31
|
+
private assertLinux;
|
|
32
|
+
private unitExists;
|
|
33
|
+
private assertOwnedUnitIfPresent;
|
|
34
|
+
private unitText;
|
|
35
|
+
install(): Promise<void>;
|
|
36
|
+
stop(): Promise<void>;
|
|
37
|
+
status(): Promise<LinuxServiceStatus>;
|
|
38
|
+
uninstall(): Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
export declare class Pm2DeviceService {
|
|
41
|
+
private platform;
|
|
42
|
+
private execFile;
|
|
43
|
+
private pm2Path;
|
|
44
|
+
private nodePath;
|
|
45
|
+
private entrypoint;
|
|
46
|
+
private user;
|
|
47
|
+
private pm2Home;
|
|
48
|
+
constructor(options?: {
|
|
49
|
+
platform?: NodeJS.Platform | string;
|
|
50
|
+
execFile?: ExecFileFn;
|
|
51
|
+
pm2Path?: string;
|
|
52
|
+
nodePath?: string;
|
|
53
|
+
entrypoint?: string;
|
|
54
|
+
user?: string;
|
|
55
|
+
pm2Home?: string;
|
|
56
|
+
});
|
|
57
|
+
private assertLinux;
|
|
58
|
+
private processRow;
|
|
59
|
+
private assertOwnedProcess;
|
|
60
|
+
private proveStartup;
|
|
61
|
+
install(): Promise<void>;
|
|
62
|
+
stop(): Promise<void>;
|
|
63
|
+
status(): Promise<LinuxServiceStatus>;
|
|
64
|
+
uninstall(): Promise<void>;
|
|
65
|
+
}
|
|
66
|
+
export declare function discoverLinuxManagers(options?: {
|
|
67
|
+
systemd?: SystemdUserDeviceService;
|
|
68
|
+
pm2?: Pm2DeviceService;
|
|
69
|
+
}): Promise<LinuxServiceStatus[]>;
|
|
70
|
+
export {};
|