@hcu-lab.me/mcp-device 1.0.6 → 1.0.7
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 +7 -71
- package/dist/bootstrap.js +1 -1
- package/dist/config-manager.js +1 -1
- package/dist/config.js +1 -1
- package/dist/custom-stdio.js +1 -2
- package/dist/data/onboarding-prompts.json +22 -11
- package/dist/device/device-state.d.ts +0 -5
- package/dist/device/device-state.js +0 -84
- package/dist/device/device.d.ts +1 -1
- package/dist/device/device.js +8 -7
- package/dist/device/execution-engine.d.ts +10 -1
- package/dist/device/execution-engine.js +81 -128
- package/dist/device/gateway-channel.d.ts +6 -0
- package/dist/device/gateway-channel.js +16 -1
- package/dist/device/gateway-tool-adapter.d.ts +3 -3
- package/dist/device/gateway-tool-adapter.js +12 -11
- package/dist/device/linux-service.d.ts +4 -0
- package/dist/device/linux-service.js +10 -3
- package/dist/device/runtime-owner.js +2 -2
- package/dist/device/windows-service.d.ts +3 -4
- package/dist/device/windows-service.js +5 -44
- package/dist/npm-scripts/remote.js +0 -2
- package/dist/server.js +9 -99
- package/dist/terminal-manager.js +1 -1
- package/dist/tools/config.d.ts +1 -1
- package/dist/tools/filesystem.js +3 -3
- package/dist/tools/pdf/markdown.js +1 -1
- package/dist/tools/schemas.d.ts +2 -3
- package/dist/tools/schemas.js +0 -10
- package/dist/ui/config-editor/index.html +1 -1
- package/dist/ui/file-preview/index.html +1 -1
- package/dist/ui/file-preview/styles.css +2 -2
- package/dist/utils/capture.d.ts +5 -28
- package/dist/utils/capture.js +11 -455
- package/dist/utils/feature-flags.d.ts +0 -47
- package/dist/utils/feature-flags.js +5 -199
- package/dist/utils/files/docx.d.ts +2 -2
- package/dist/utils/files/docx.js +2 -2
- package/dist/utils/usageTracker.js +0 -24
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -6
- package/dist/tools/feedback.d.ts +0 -8
- package/dist/tools/feedback.js +0 -132
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
4
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
@@ -8,134 +8,109 @@ import { captureRemote } from '../utils/capture.js';
|
|
|
8
8
|
import { deviceStatePaths } from './device-state.js';
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
10
|
const __dirname = path.dirname(__filename);
|
|
11
|
-
export class
|
|
11
|
+
export class LocalExecutionEngine {
|
|
12
12
|
constructor() {
|
|
13
13
|
this.mcpClient = null;
|
|
14
14
|
this.mcpTransport = null;
|
|
15
15
|
this.isReady = false;
|
|
16
|
+
this.runtimeReason = 'LOCAL_EXECUTION_ENGINE_UNAVAILABLE';
|
|
17
|
+
this.runtimeGeneration = null;
|
|
18
|
+
}
|
|
19
|
+
markUnavailable(client) {
|
|
20
|
+
if (client && this.mcpClient !== client)
|
|
21
|
+
return;
|
|
22
|
+
this.isReady = false;
|
|
23
|
+
this.runtimeReason = 'LOCAL_EXECUTION_ENGINE_UNAVAILABLE';
|
|
24
|
+
this.runtimeGeneration = null;
|
|
16
25
|
}
|
|
17
26
|
async initialize() {
|
|
18
|
-
console.debug('[DEBUG]
|
|
19
|
-
|
|
20
|
-
if (!config) {
|
|
21
|
-
console.debug('[DEBUG] No MCP config found');
|
|
22
|
-
throw new Error('Desktop Commander MCP not found. Please install it globally via `npm install -g @wonderwhy-er/desktop-commander` or build the local project.');
|
|
23
|
-
}
|
|
24
|
-
console.log(` - â³ Connecting to Local Desktop Commander MCP using: ${config.command} ${config.args.join(' ')}`);
|
|
25
|
-
console.debug('[DEBUG] MCP config:', JSON.stringify(config, null, 2));
|
|
27
|
+
console.debug('[DEBUG] LocalExecutionEngine.initialize() called');
|
|
28
|
+
this.markUnavailable();
|
|
26
29
|
try {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
this.mcpTransport = new StdioClientTransport({
|
|
30
|
+
const config = await this.resolveMcpConfig();
|
|
31
|
+
if (!config)
|
|
32
|
+
throw new Error('Bundled local execution engine is unavailable. Reinstall MCP Device.');
|
|
33
|
+
const transport = new StdioClientTransport({
|
|
32
34
|
...config,
|
|
33
35
|
env: {
|
|
34
36
|
...getDefaultEnvironment(),
|
|
35
37
|
...config.env,
|
|
36
|
-
MCP_DEVICE_REMOTE: 'true'
|
|
37
|
-
DESKTOP_COMMANDER_DISABLE_TELEMETRY: 'true'
|
|
38
|
+
MCP_DEVICE_REMOTE: 'true'
|
|
38
39
|
}
|
|
39
40
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
this.mcpClient =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
console.debug('[DEBUG] Connecting MCP client to transport');
|
|
50
|
-
await this.mcpClient.connect(this.mcpTransport);
|
|
41
|
+
const client = new Client({ name: 'mcp-device-local-engine', version: '1.0.0' }, { capabilities: {} });
|
|
42
|
+
this.mcpTransport = transport;
|
|
43
|
+
this.mcpClient = client;
|
|
44
|
+
client.onclose = () => this.markUnavailable(client);
|
|
45
|
+
await client.connect(transport);
|
|
46
|
+
if (this.mcpClient !== client || this.mcpTransport !== transport || transport.pid === null) {
|
|
47
|
+
throw new Error('Local execution engine closed during startup.');
|
|
48
|
+
}
|
|
49
|
+
this.runtimeGeneration = randomUUID();
|
|
51
50
|
this.isReady = true;
|
|
52
|
-
|
|
53
|
-
console.
|
|
51
|
+
this.runtimeReason = null;
|
|
52
|
+
console.log(' - Local execution engine ready');
|
|
54
53
|
}
|
|
55
54
|
catch (error) {
|
|
56
|
-
console.error(' -
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
console.error(' - Failed to start local execution engine:', error);
|
|
56
|
+
await captureRemote('local_engine_init_failed', { error });
|
|
57
|
+
try {
|
|
58
|
+
await this.mcpClient?.close();
|
|
59
|
+
}
|
|
60
|
+
catch { }
|
|
61
|
+
try {
|
|
62
|
+
await this.mcpTransport?.close();
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
this.mcpClient = null;
|
|
66
|
+
this.mcpTransport = null;
|
|
67
|
+
this.markUnavailable();
|
|
60
68
|
}
|
|
61
69
|
}
|
|
62
70
|
async resolveMcpConfig() {
|
|
63
|
-
|
|
64
|
-
// Option 1: Development/Local Build
|
|
65
|
-
// Adjusting path resolution since we are now in src/device and dist is in root/dist
|
|
66
|
-
// Original: path.resolve(__dirname, '../../dist/index.js')
|
|
67
|
-
const devPath = path.resolve(__dirname, '../../dist/index.js');
|
|
68
|
-
console.debug('[DEBUG] Checking local dev path:', devPath);
|
|
71
|
+
const localEngine = path.resolve(__dirname, '../../dist/index.js');
|
|
69
72
|
try {
|
|
70
|
-
await fs.access(
|
|
71
|
-
console.debug(' - 🔠Found local MCP server at:', devPath);
|
|
72
|
-
const runtimeDir = deviceStatePaths().runtime;
|
|
73
|
-
await fs.mkdir(runtimeDir, { recursive: true, mode: 0o700 });
|
|
74
|
-
return {
|
|
75
|
-
command: process.execPath, // Use the current node executable
|
|
76
|
-
args: [devPath],
|
|
77
|
-
cwd: runtimeDir
|
|
78
|
-
};
|
|
73
|
+
await fs.access(localEngine);
|
|
79
74
|
}
|
|
80
75
|
catch {
|
|
81
|
-
|
|
82
|
-
// Local file not found, continue...
|
|
76
|
+
return null;
|
|
83
77
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const whichCommand = process.platform === 'win32' ? 'where' : 'which';
|
|
92
|
-
console.debug('[DEBUG] Using platform command:', whichCommand, 'on platform:', process.platform);
|
|
93
|
-
const check = spawn(whichCommand, [commandName], { windowsHide: true }); // Prevent visible console windows on Windows
|
|
94
|
-
check.on('error', (err) => {
|
|
95
|
-
console.debug('[DEBUG] Spawn error for', whichCommand, ':', err.message);
|
|
96
|
-
reject(err);
|
|
97
|
-
});
|
|
98
|
-
check.on('close', (code) => {
|
|
99
|
-
console.debug('[DEBUG]', whichCommand, 'exited with code:', code);
|
|
100
|
-
return code === 0 ? resolve() : reject(new Error('Command not found'));
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
console.debug(' - Found global desktop-commander CLI');
|
|
104
|
-
return {
|
|
105
|
-
command: commandName,
|
|
106
|
-
args: []
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
catch (err) {
|
|
110
|
-
console.debug('[DEBUG] Global command not found:', err);
|
|
111
|
-
// Global command not found
|
|
112
|
-
}
|
|
113
|
-
console.debug('[DEBUG] No MCP config resolved');
|
|
114
|
-
return null;
|
|
78
|
+
const runtimeDir = deviceStatePaths().runtime;
|
|
79
|
+
await fs.mkdir(runtimeDir, { recursive: true, mode: 0o700 });
|
|
80
|
+
return {
|
|
81
|
+
command: process.execPath,
|
|
82
|
+
args: [localEngine],
|
|
83
|
+
cwd: runtimeDir
|
|
84
|
+
};
|
|
115
85
|
}
|
|
116
86
|
getChildPid() {
|
|
117
87
|
return this.mcpTransport?.pid ?? null;
|
|
118
88
|
}
|
|
89
|
+
getRuntimeState() {
|
|
90
|
+
return {
|
|
91
|
+
runtime_ready: this.isReady,
|
|
92
|
+
runtime_reason: this.isReady ? null : this.runtimeReason,
|
|
93
|
+
execution_runtime_generation: this.isReady ? this.runtimeGeneration : null
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
assertReady() {
|
|
97
|
+
if (this.isReady && this.mcpClient)
|
|
98
|
+
return;
|
|
99
|
+
const error = new Error('Local execution engine is unavailable.');
|
|
100
|
+
error.code = 'DEVICE_NOT_READY';
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
119
103
|
async callClientTool(toolName, args, metadata) {
|
|
120
|
-
|
|
121
|
-
console.debug('[DEBUG] callClientTool() failed - not ready or no client');
|
|
122
|
-
throw new Error('DesktopIntegration not initialized');
|
|
123
|
-
}
|
|
124
|
-
// Proxy other tools to MCP server
|
|
104
|
+
this.assertReady();
|
|
125
105
|
try {
|
|
126
|
-
|
|
127
|
-
const result = await this.mcpClient.callTool({
|
|
106
|
+
return await this.mcpClient.callTool({
|
|
128
107
|
name: toolName,
|
|
129
108
|
arguments: args,
|
|
130
109
|
_meta: { remote: true, ...metadata || {} }
|
|
131
110
|
});
|
|
132
|
-
console.debug('[DEBUG] Tool call successful:', toolName);
|
|
133
|
-
return result;
|
|
134
111
|
}
|
|
135
112
|
catch (error) {
|
|
136
|
-
|
|
137
|
-
console.debug('[DEBUG] Tool call error details:', error);
|
|
138
|
-
await captureRemote('desktop_integration_tool_call_failed', { error, toolName });
|
|
113
|
+
await captureRemote('local_engine_tool_call_failed', { error, toolName });
|
|
139
114
|
throw error;
|
|
140
115
|
}
|
|
141
116
|
}
|
|
@@ -143,59 +118,37 @@ export class DesktopCommanderIntegration {
|
|
|
143
118
|
if (!this.mcpClient)
|
|
144
119
|
return { tools: [] };
|
|
145
120
|
try {
|
|
146
|
-
// List tools from MCP server
|
|
147
121
|
const mcpTools = await this.mcpClient.listTools();
|
|
148
|
-
|
|
149
|
-
return {
|
|
150
|
-
tools: mcpTools.tools || []
|
|
151
|
-
};
|
|
122
|
+
return { tools: mcpTools.tools || [] };
|
|
152
123
|
}
|
|
153
124
|
catch (error) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// Fallback to local tools
|
|
157
|
-
return {
|
|
158
|
-
tools: []
|
|
159
|
-
};
|
|
125
|
+
await captureRemote('local_engine_list_tools_failed', { error });
|
|
126
|
+
return { tools: [] };
|
|
160
127
|
}
|
|
161
128
|
}
|
|
162
129
|
async shutdown() {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error(`${name} timeout after ${timeoutMs}ms`)), timeoutMs))
|
|
168
|
-
]);
|
|
169
|
-
};
|
|
130
|
+
const closeWithTimeout = async (operation, name, timeoutMs = 3000) => Promise.race([
|
|
131
|
+
operation(),
|
|
132
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`${name} timeout after ${timeoutMs}ms`)), timeoutMs))
|
|
133
|
+
]);
|
|
170
134
|
if (this.mcpClient) {
|
|
171
135
|
try {
|
|
172
|
-
console.log(' → Closing MCP client...');
|
|
173
|
-
console.debug('[DEBUG] Calling mcpClient.close() with timeout');
|
|
174
136
|
await closeWithTimeout(() => this.mcpClient.close(), 'MCP client close');
|
|
175
|
-
console.log(' ✓ MCP client closed');
|
|
176
137
|
}
|
|
177
|
-
catch (
|
|
178
|
-
|
|
179
|
-
console.debug('[DEBUG] MCP client close error:', e);
|
|
180
|
-
await captureRemote('desktop_integration_shutdown_error', { error: e, component: 'client' });
|
|
138
|
+
catch (error) {
|
|
139
|
+
await captureRemote('local_engine_shutdown_error', { error, component: 'client' });
|
|
181
140
|
}
|
|
182
141
|
this.mcpClient = null;
|
|
183
142
|
}
|
|
184
143
|
if (this.mcpTransport) {
|
|
185
144
|
try {
|
|
186
|
-
console.log(' → Closing MCP transport...');
|
|
187
|
-
console.debug('[DEBUG] Calling mcpTransport.close() with timeout');
|
|
188
145
|
await closeWithTimeout(() => this.mcpTransport.close(), 'MCP transport close');
|
|
189
|
-
console.log(' ✓ MCP transport closed');
|
|
190
146
|
}
|
|
191
|
-
catch (
|
|
192
|
-
|
|
193
|
-
console.debug('[DEBUG] MCP transport close error:', e);
|
|
194
|
-
await captureRemote('desktop_integration_shutdown_error', { error: e, component: 'transport' });
|
|
147
|
+
catch (error) {
|
|
148
|
+
await captureRemote('local_engine_shutdown_error', { error, component: 'transport' });
|
|
195
149
|
}
|
|
196
150
|
this.mcpTransport = null;
|
|
197
151
|
}
|
|
198
|
-
this.
|
|
199
|
-
console.debug('[DEBUG] Desktop Commander integration shutdown complete');
|
|
152
|
+
this.markUnavailable();
|
|
200
153
|
}
|
|
201
154
|
}
|
|
@@ -14,6 +14,11 @@ export interface GatewayChannelOptions {
|
|
|
14
14
|
securityProtocolFloor?: number;
|
|
15
15
|
appCaPem?: string;
|
|
16
16
|
adapter: GatewayToolAdapter;
|
|
17
|
+
runtimeState?: () => {
|
|
18
|
+
runtime_ready: boolean;
|
|
19
|
+
runtime_reason: string | null;
|
|
20
|
+
execution_runtime_generation: string | null;
|
|
21
|
+
};
|
|
17
22
|
agentVersion?: string;
|
|
18
23
|
onStatus?: (payload: any) => void | Promise<void>;
|
|
19
24
|
onUpdateRequest?: (targetVersion: string, context: {
|
|
@@ -40,6 +45,7 @@ export declare class GatewayDeviceChannel {
|
|
|
40
45
|
private authMode;
|
|
41
46
|
private updateInFlight?;
|
|
42
47
|
constructor(options: GatewayChannelOptions);
|
|
48
|
+
private runtimePayload;
|
|
43
49
|
start(): Promise<void>;
|
|
44
50
|
private startV2Transport;
|
|
45
51
|
stop(): Promise<void>;
|
|
@@ -19,6 +19,19 @@ export class GatewayDeviceChannel {
|
|
|
19
19
|
this.authMode = 'reconnect';
|
|
20
20
|
this.identity = options.identity || new GatewayDeviceIdentity();
|
|
21
21
|
}
|
|
22
|
+
runtimePayload() {
|
|
23
|
+
if (!this.options.runtimeState)
|
|
24
|
+
return {};
|
|
25
|
+
const state = this.options.runtimeState();
|
|
26
|
+
const ready = state?.runtime_ready === true;
|
|
27
|
+
const reason = ready ? null : String(state?.runtime_reason || 'LOCAL_EXECUTION_ENGINE_UNAVAILABLE').slice(0, 96);
|
|
28
|
+
const generation = String(state?.execution_runtime_generation || '').trim().slice(0, 128) || null;
|
|
29
|
+
return {
|
|
30
|
+
runtime_ready: ready,
|
|
31
|
+
runtime_reason: reason,
|
|
32
|
+
execution_runtime_generation: generation
|
|
33
|
+
};
|
|
34
|
+
}
|
|
22
35
|
async start() {
|
|
23
36
|
this.authenticatedDeviceId = undefined;
|
|
24
37
|
this.connectionEpoch = undefined;
|
|
@@ -150,6 +163,7 @@ export class GatewayDeviceChannel {
|
|
|
150
163
|
arch: process.arch,
|
|
151
164
|
path_style: process.platform === 'win32' ? 'windows' : 'posix',
|
|
152
165
|
capabilities: [...GATEWAY_CAPABILITIES],
|
|
166
|
+
...this.runtimePayload(),
|
|
153
167
|
...(enrolling || pairing ? { public_key_pem: record.publicKeyPem } : {})
|
|
154
168
|
}
|
|
155
169
|
});
|
|
@@ -173,6 +187,7 @@ export class GatewayDeviceChannel {
|
|
|
173
187
|
arch: process.arch,
|
|
174
188
|
path_style: process.platform === 'win32' ? 'windows' : 'posix',
|
|
175
189
|
capabilities: [...GATEWAY_CAPABILITIES],
|
|
190
|
+
...this.runtimePayload(),
|
|
176
191
|
...(pairing || enrolling ? { public_key_pem: record.publicKeyPem, enrollment_grant: grant } : {})
|
|
177
192
|
}
|
|
178
193
|
});
|
|
@@ -432,7 +447,7 @@ export class GatewayDeviceChannel {
|
|
|
432
447
|
type: 'heartbeat',
|
|
433
448
|
device_id: record.deviceId,
|
|
434
449
|
timestamp: Date.now(),
|
|
435
|
-
payload:
|
|
450
|
+
payload: this.runtimePayload()
|
|
436
451
|
});
|
|
437
452
|
}, HEARTBEAT_MS);
|
|
438
453
|
this.heartbeat.unref?.();
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { LocalExecutionEngine } from './execution-engine.js';
|
|
2
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
3
|
export interface GatewayToolAdapterOptions {
|
|
4
4
|
allowedRoots?: string[];
|
|
5
5
|
pathValidator?: (requestedPath: string) => Promise<string>;
|
|
6
6
|
}
|
|
7
7
|
export declare class GatewayToolAdapter {
|
|
8
|
-
private
|
|
8
|
+
private engine;
|
|
9
9
|
private allowedRoots;
|
|
10
10
|
private pathValidator;
|
|
11
11
|
private canonicalRoots?;
|
|
12
|
-
constructor(
|
|
12
|
+
constructor(engine: LocalExecutionEngine, options?: GatewayToolAdapterOptions);
|
|
13
13
|
private guardPath;
|
|
14
14
|
private imagePreview;
|
|
15
15
|
call(tool: string, args?: any): Promise<any>;
|
|
@@ -63,7 +63,7 @@ function parsePid(result) {
|
|
|
63
63
|
const match = textFromResult(result).match(/PID\s+(-?\d+)/i);
|
|
64
64
|
const pid = Number(match?.[1]);
|
|
65
65
|
if (!Number.isInteger(pid))
|
|
66
|
-
throw new Error('
|
|
66
|
+
throw new Error('Local execution engine did not return a process PID');
|
|
67
67
|
return pid;
|
|
68
68
|
}
|
|
69
69
|
async function runShell(args) {
|
|
@@ -120,8 +120,8 @@ async function runShell(args) {
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
export class GatewayToolAdapter {
|
|
123
|
-
constructor(
|
|
124
|
-
this.
|
|
123
|
+
constructor(engine, options = {}) {
|
|
124
|
+
this.engine = engine;
|
|
125
125
|
this.allowedRoots = options.allowedRoots ?? configuredGatewayRoots();
|
|
126
126
|
this.pathValidator = options.pathValidator ?? validatePath;
|
|
127
127
|
}
|
|
@@ -188,6 +188,7 @@ export class GatewayToolAdapter {
|
|
|
188
188
|
throw new Error(`REMOTE_IMAGE_TOO_LARGE: unable to fit preview within ${MAX_REMOTE_IMAGE_PREVIEW_BYTES} bytes`);
|
|
189
189
|
}
|
|
190
190
|
async call(tool, args = {}) {
|
|
191
|
+
this.engine.assertReady();
|
|
191
192
|
if (tool === 'read_text_file') {
|
|
192
193
|
if (args.head !== undefined && args.tail !== undefined)
|
|
193
194
|
throw new Error('Use either head or tail, not both');
|
|
@@ -199,10 +200,10 @@ export class GatewayToolAdapter {
|
|
|
199
200
|
else if (args.tail !== undefined) {
|
|
200
201
|
mapped.offset = -Number(args.tail);
|
|
201
202
|
}
|
|
202
|
-
return assertSuccess(await this.
|
|
203
|
+
return assertSuccess(await this.engine.callClientTool('read_file', mapped), 'read_file');
|
|
203
204
|
}
|
|
204
205
|
if (tool === 'write_file') {
|
|
205
|
-
return assertSuccess(await this.
|
|
206
|
+
return assertSuccess(await this.engine.callClientTool('write_file', {
|
|
206
207
|
path: await this.guardPath(args.path),
|
|
207
208
|
content: args.content,
|
|
208
209
|
mode: 'rewrite'
|
|
@@ -214,7 +215,7 @@ export class GatewayToolAdapter {
|
|
|
214
215
|
return await runShell({ ...args, working_directory: await this.guardPath(args.working_directory) });
|
|
215
216
|
if (tool === 'start_process') {
|
|
216
217
|
const workingDirectory = await this.guardPath(args.working_directory);
|
|
217
|
-
const result = assertSuccess(await this.
|
|
218
|
+
const result = assertSuccess(await this.engine.callClientTool('start_process', {
|
|
218
219
|
command: args.command,
|
|
219
220
|
timeout_ms: Number(args.timeout_ms || 10000),
|
|
220
221
|
working_directory: workingDirectory
|
|
@@ -222,7 +223,7 @@ export class GatewayToolAdapter {
|
|
|
222
223
|
return { ...result, pid: parsePid(result), session_id: String(parsePid(result)) };
|
|
223
224
|
}
|
|
224
225
|
if (tool === 'read_process_output') {
|
|
225
|
-
return assertSuccess(await this.
|
|
226
|
+
return assertSuccess(await this.engine.callClientTool('read_process_output', {
|
|
226
227
|
pid: Number(args.session_id),
|
|
227
228
|
offset: args.offset,
|
|
228
229
|
length: args.length,
|
|
@@ -230,14 +231,14 @@ export class GatewayToolAdapter {
|
|
|
230
231
|
}), 'read_process_output');
|
|
231
232
|
}
|
|
232
233
|
if (tool === 'interact_with_process') {
|
|
233
|
-
return assertSuccess(await this.
|
|
234
|
+
return assertSuccess(await this.engine.callClientTool('interact_with_process', {
|
|
234
235
|
pid: Number(args.session_id),
|
|
235
236
|
input: String(args.input ?? ''),
|
|
236
237
|
timeout_ms: Number(args.timeout_ms || 8000)
|
|
237
238
|
}), 'interact_with_process');
|
|
238
239
|
}
|
|
239
240
|
if (tool === 'terminate_process') {
|
|
240
|
-
return assertSuccess(await this.
|
|
241
|
+
return assertSuccess(await this.engine.callClientTool('force_terminate', {
|
|
241
242
|
pid: Number(args.session_id)
|
|
242
243
|
}), 'force_terminate');
|
|
243
244
|
}
|
|
@@ -258,7 +259,7 @@ export class GatewayToolAdapter {
|
|
|
258
259
|
throw new Error('expected_replacements must be a positive integer');
|
|
259
260
|
}
|
|
260
261
|
if (args.dry_run === true) {
|
|
261
|
-
const read = assertSuccess(await this.
|
|
262
|
+
const read = assertSuccess(await this.engine.callClientTool('read_file', {
|
|
262
263
|
path: args.path,
|
|
263
264
|
offset: 0,
|
|
264
265
|
length: 100000
|
|
@@ -274,7 +275,7 @@ export class GatewayToolAdapter {
|
|
|
274
275
|
}) }]
|
|
275
276
|
};
|
|
276
277
|
}
|
|
277
|
-
return assertSuccess(await this.
|
|
278
|
+
return assertSuccess(await this.engine.callClientTool('edit_block', {
|
|
278
279
|
file_path: args.path,
|
|
279
280
|
old_string: args.old_text,
|
|
280
281
|
new_string: args.new_text,
|
|
@@ -22,6 +22,7 @@ export declare class SystemdUserDeviceService {
|
|
|
22
22
|
private nodePath;
|
|
23
23
|
private entrypoint;
|
|
24
24
|
private pathValue;
|
|
25
|
+
private runtimeDir;
|
|
25
26
|
constructor(options?: {
|
|
26
27
|
platform?: NodeJS.Platform | string;
|
|
27
28
|
execFile?: ExecFileFn;
|
|
@@ -29,6 +30,7 @@ export declare class SystemdUserDeviceService {
|
|
|
29
30
|
nodePath?: string;
|
|
30
31
|
entrypoint?: string;
|
|
31
32
|
pathValue?: string;
|
|
33
|
+
runtimeDir?: string;
|
|
32
34
|
});
|
|
33
35
|
private assertLinux;
|
|
34
36
|
private unitExists;
|
|
@@ -47,6 +49,7 @@ export declare class Pm2DeviceService {
|
|
|
47
49
|
private entrypoint;
|
|
48
50
|
private user;
|
|
49
51
|
private pm2Home;
|
|
52
|
+
private runtimeDir;
|
|
50
53
|
constructor(options?: {
|
|
51
54
|
platform?: NodeJS.Platform | string;
|
|
52
55
|
execFile?: ExecFileFn;
|
|
@@ -55,6 +58,7 @@ export declare class Pm2DeviceService {
|
|
|
55
58
|
entrypoint?: string;
|
|
56
59
|
user?: string;
|
|
57
60
|
pm2Home?: string;
|
|
61
|
+
runtimeDir?: string;
|
|
58
62
|
});
|
|
59
63
|
private assertLinux;
|
|
60
64
|
private processRow;
|
|
@@ -4,6 +4,7 @@ import os from 'os';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import readline from 'readline/promises';
|
|
6
6
|
import { promisify } from 'util';
|
|
7
|
+
import { deviceStatePaths } from './device-state.js';
|
|
7
8
|
const execFileAsync = promisify(execFileCallback);
|
|
8
9
|
export const SYSTEMD_UNIT = 'mcp-device.service';
|
|
9
10
|
const SYSTEMD_OWNER_MARKER = '# Managed by MCP Device';
|
|
@@ -55,6 +56,7 @@ export class SystemdUserDeviceService {
|
|
|
55
56
|
this.nodePath = options.platform === 'linux' && options.nodePath ? String(options.nodePath) : path.resolve(options.nodePath || process.execPath);
|
|
56
57
|
this.entrypoint = options.platform === 'linux' && options.entrypoint ? String(options.entrypoint) : path.resolve(options.entrypoint || process.argv[1]);
|
|
57
58
|
this.pathValue = String(options.pathValue ?? process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin');
|
|
59
|
+
this.runtimeDir = options.platform === 'linux' && options.runtimeDir ? String(options.runtimeDir) : path.resolve(options.runtimeDir || deviceStatePaths().runtime);
|
|
58
60
|
}
|
|
59
61
|
assertLinux() {
|
|
60
62
|
if (this.platform !== 'linux')
|
|
@@ -89,6 +91,7 @@ export class SystemdUserDeviceService {
|
|
|
89
91
|
'[Service]',
|
|
90
92
|
'Type=simple',
|
|
91
93
|
`Environment="PATH=${this.pathValue.replace(/["\\]/g, match => `\\${match}`)}"`,
|
|
94
|
+
`WorkingDirectory=${quoteSystemdArg(this.runtimeDir)}`,
|
|
92
95
|
`ExecStart=${quoteSystemdArg(this.nodePath)} ${quoteSystemdArg(this.entrypoint)} --service --manager=systemd`,
|
|
93
96
|
'Restart=on-failure',
|
|
94
97
|
'RestartSec=3',
|
|
@@ -102,6 +105,7 @@ export class SystemdUserDeviceService {
|
|
|
102
105
|
this.assertLinux();
|
|
103
106
|
await this.assertOwnedUnitIfPresent();
|
|
104
107
|
await this.execFile('systemctl', ['--user', 'show-environment']);
|
|
108
|
+
await fs.mkdir(this.runtimeDir, { recursive: true, mode: 0o700 });
|
|
105
109
|
await fs.mkdir(path.dirname(this.unitPath), { recursive: true });
|
|
106
110
|
await fs.writeFile(this.unitPath, this.unitText(), { mode: 0o600 });
|
|
107
111
|
await this.execFile('systemctl', ['--user', 'daemon-reload']);
|
|
@@ -152,6 +156,7 @@ export class Pm2DeviceService {
|
|
|
152
156
|
this.entrypoint = options.platform === 'linux' && options.entrypoint ? String(options.entrypoint) : path.resolve(options.entrypoint || process.argv[1]);
|
|
153
157
|
this.user = String(options.user || process.env.USER || process.env.LOGNAME || os.userInfo().username);
|
|
154
158
|
this.pm2Home = options.platform === 'linux' && options.pm2Home ? String(options.pm2Home) : path.resolve(options.pm2Home || process.env.PM2_HOME || path.join(os.homedir(), '.pm2'));
|
|
159
|
+
this.runtimeDir = options.platform === 'linux' && options.runtimeDir ? String(options.runtimeDir) : path.resolve(options.runtimeDir || deviceStatePaths().runtime);
|
|
155
160
|
}
|
|
156
161
|
assertLinux() {
|
|
157
162
|
if (this.platform !== 'linux')
|
|
@@ -209,13 +214,15 @@ export class Pm2DeviceService {
|
|
|
209
214
|
}
|
|
210
215
|
async install() {
|
|
211
216
|
await this.proveStartup();
|
|
217
|
+
await fs.mkdir(this.runtimeDir, { recursive: true, mode: 0o700 });
|
|
212
218
|
const existing = await this.processRow();
|
|
213
219
|
this.assertOwnedProcess(existing);
|
|
214
220
|
if (!existing) {
|
|
215
|
-
await this.execFile(this.pm2Path, ['start', this.entrypoint, '--name', PM2_NAME, '--interpreter', this.nodePath, '--', '--service', '--manager=pm2']);
|
|
221
|
+
await this.execFile(this.pm2Path, ['start', this.entrypoint, '--name', PM2_NAME, '--interpreter', this.nodePath, '--cwd', this.runtimeDir, '--', '--service', '--manager=pm2']);
|
|
216
222
|
}
|
|
217
|
-
else if (String(existing?.pm2_env?.status || '').toLowerCase() !== 'online'
|
|
218
|
-
|
|
223
|
+
else if (String(existing?.pm2_env?.status || '').toLowerCase() !== 'online' ||
|
|
224
|
+
String(existing?.pm2_env?.pm_cwd || '') !== this.runtimeDir) {
|
|
225
|
+
await this.execFile(this.pm2Path, ['restart', PM2_NAME, '--cwd', this.runtimeDir]);
|
|
219
226
|
}
|
|
220
227
|
await this.execFile(this.pm2Path, ['save']);
|
|
221
228
|
const status = await this.status();
|
|
@@ -2,7 +2,7 @@ import crypto from 'crypto';
|
|
|
2
2
|
import fs from 'fs/promises';
|
|
3
3
|
import net from 'net';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import { deviceStatePaths
|
|
5
|
+
import { deviceStatePaths } from './device-state.js';
|
|
6
6
|
function endpointFor(paths, platform = process.platform) {
|
|
7
7
|
const digest = crypto.createHash('sha256').update(paths.root, 'utf8').digest('hex').slice(0, 24);
|
|
8
8
|
if (platform === 'win32')
|
|
@@ -106,7 +106,7 @@ export class RuntimeOwner {
|
|
|
106
106
|
await fs.writeFile(runtimeOwnerControlFileFor(this.paths), JSON.stringify({ endpoint: this.endpoint, nonce: this.nonce }, null, 2), { mode: 0o600 });
|
|
107
107
|
}
|
|
108
108
|
async bootstrap() {
|
|
109
|
-
await
|
|
109
|
+
await fs.mkdir(this.paths.root, { recursive: true, mode: 0o700 });
|
|
110
110
|
}
|
|
111
111
|
setDeviceId(deviceId) {
|
|
112
112
|
this.status.deviceId = String(deviceId || '').trim() || null;
|
|
@@ -9,6 +9,7 @@ export declare function buildWindowsRunnerScript(options: {
|
|
|
9
9
|
allowedRoots?: string;
|
|
10
10
|
nodePath: string;
|
|
11
11
|
entrypoint: string;
|
|
12
|
+
runtimeDir?: string;
|
|
12
13
|
}): string;
|
|
13
14
|
export type WindowsDeviceServiceStatus = {
|
|
14
15
|
installed: boolean;
|
|
@@ -21,24 +22,22 @@ export declare class WindowsDeviceService {
|
|
|
21
22
|
private platform;
|
|
22
23
|
private execFile;
|
|
23
24
|
private runnerPath;
|
|
24
|
-
private legacyRunnerPath;
|
|
25
25
|
private nodePath;
|
|
26
26
|
private entrypoint;
|
|
27
|
+
private runtimeDir;
|
|
27
28
|
constructor(options?: {
|
|
28
29
|
platform?: NodeJS.Platform | string;
|
|
29
30
|
execFile?: ExecFileFn;
|
|
30
31
|
runnerPath?: string;
|
|
31
|
-
legacyRunnerPath?: string;
|
|
32
32
|
nodePath?: string;
|
|
33
33
|
entrypoint?: string;
|
|
34
|
+
runtimeDir?: string;
|
|
34
35
|
});
|
|
35
36
|
private assertWindows;
|
|
36
37
|
private runnerExists;
|
|
37
38
|
private hasUserRunEntry;
|
|
38
39
|
private scheduledTaskState;
|
|
39
40
|
private deleteRegistrations;
|
|
40
|
-
private commandReferencesLegacyRunner;
|
|
41
|
-
private deleteVerifiedLegacyRegistrations;
|
|
42
41
|
private createAutostartRegistration;
|
|
43
42
|
private startUserProcess;
|
|
44
43
|
private userProcessRunning;
|