@hcu-lab.me/mcp-device 1.0.5 → 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 +56 -19
- 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 +2 -5
- package/dist/device/device-state.js +3 -85
- package/dist/device/device.d.ts +1 -1
- package/dist/device/device.js +69 -9
- package/dist/device/execution-engine.d.ts +11 -1
- package/dist/device/execution-engine.js +85 -126
- package/dist/device/gateway-channel.d.ts +19 -1
- package/dist/device/gateway-channel.js +107 -21
- package/dist/device/gateway-identity.d.ts +1 -0
- package/dist/device/gateway-identity.js +4 -0
- 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 +6 -0
- package/dist/device/linux-service.js +12 -5
- package/dist/device/runtime-owner.d.ts +1 -0
- package/dist/device/runtime-owner.js +6 -6
- 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/device/windows-service.d.ts +3 -4
- package/dist/device/windows-service.js +5 -44
- 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 -3
- package/dist/server.js +83 -167
- package/dist/terminal-manager.js +41 -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/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 -24
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +6 -7
- package/dist/tools/feedback.d.ts +0 -8
- package/dist/tools/feedback.js +0 -132
package/README.md
CHANGED
|
@@ -1,90 +1,26 @@
|
|
|
1
1
|
# MCP Device
|
|
2
2
|
|
|
3
|
-
`@hcu-lab.me/mcp-device` connects a user-owned computer to the authenticated MCP Gateway. The device opens the outbound connection; no inbound device port is required.
|
|
4
|
-
|
|
5
3
|
The gateway owns accounts, OAuth, device ownership, MCP tool contracts, routing, and usage aggregation. MCP Device owns local device identity, pairing, reconnect lifecycle, bounded execution, background registration, and local status.
|
|
6
4
|
|
|
7
|
-
##
|
|
8
|
-
|
|
9
|
-
Canonical production gateway: https://device.hcu-lab.me
|
|
10
|
-
|
|
11
|
-
- Dashboard: https://device.hcu-lab.me/dashboard
|
|
12
|
-
- Pair device: https://device.hcu-lab.me/pair
|
|
13
|
-
- Live setup/help: https://device.hcu-lab.me/help
|
|
14
|
-
|
|
15
|
-
Install the current release:
|
|
5
|
+
## Install
|
|
16
6
|
|
|
17
7
|
```powershell
|
|
18
|
-
npm install -g @hcu-lab.me/mcp-device
|
|
8
|
+
npm install -g @hcu-lab.me/mcp-device@1.0.7
|
|
19
9
|
mcp-device login
|
|
20
10
|
mcp-device install
|
|
21
11
|
mcp-device status
|
|
22
12
|
```
|
|
23
13
|
|
|
24
|
-
`mcp-device login` stays terminal-only: it prints the pairing URL, user code, and QR code, and never opens a browser automatically.
|
|
25
|
-
|
|
26
|
-
## Limited hosted access
|
|
27
|
-
|
|
28
|
-
The public source package remains MIT licensed, but access to the hosted `device.hcu-lab.me` gateway is intentionally limited. New hosted accounts are admitted through a small, controlled supply of invite codes rather than open registration. Invite availability may be capped, paused, expired, or revoked as the service capacity and security policy change. Installing the package does not by itself grant a hosted-service account or guarantee that an invite code will be available.
|
|
29
|
-
|
|
30
|
-
The hosted Pair/Help pages render their gateway URL and navigation from the deployment that served the page, so staging or custom deployments do not need a production-host rewrite. For the canonical package, `https://device.hcu-lab.me` is the default gateway; `MCP_GATEWAY_URL` is only needed when intentionally using another gateway.
|
|
31
|
-
|
|
32
14
|
## Commands
|
|
33
15
|
|
|
34
16
|
```text
|
|
35
17
|
mcp-device start in the foreground
|
|
36
18
|
mcp-device start start in the foreground
|
|
37
|
-
mcp-device login
|
|
19
|
+
mcp-device login link or switch the gateway account
|
|
38
20
|
mcp-device logout unlink the gateway account from this device
|
|
39
|
-
mcp-device status show
|
|
40
|
-
mcp-device install pair if needed, then install and start
|
|
41
|
-
mcp-device stop stop the current runtime
|
|
42
|
-
mcp-device uninstall remove background registration while preserving identity
|
|
21
|
+
mcp-device status show device and runtime status
|
|
22
|
+
mcp-device install pair if needed, then install and start the background runtime
|
|
23
|
+
mcp-device stop stop the current runtime
|
|
24
|
+
mcp-device uninstall remove background registration while preserving device identity
|
|
43
25
|
mcp-device --help show command help
|
|
44
26
|
```
|
|
45
|
-
|
|
46
|
-
`md` is an alias of `mcp-device` on POSIX shells. On Windows, use `mcp-device`; bare `md` has a shell collision with built-in directory-creation commands/aliases and is not the supported invocation.
|
|
47
|
-
|
|
48
|
-
On Windows, `install` uses current-user background registration. On Linux, every `install` asks whether to use `systemd --user` or an already configured PM2 installation. MCP Device does not install PM2, run `pm2 startup`, or use `sudo` automatically.
|
|
49
|
-
|
|
50
|
-
## Dashboard updates
|
|
51
|
-
|
|
52
|
-
Starting with MCP Device 1.0.5, an installed background device reports its package version to the authenticated gateway and can accept a per-device update requested by its owning account from the dashboard. The control is intentionally narrow: the gateway supplies only an exact stable version, and the device installs only `@hcu-lab.me/mcp-device@<version>`, verifies the installed version, then restarts through its existing Windows, systemd-user, or PM2 lifecycle.
|
|
53
|
-
|
|
54
|
-
Installations older than 1.0.5 do not understand this update control. Upgrade those devices to 1.0.5 once with npm; subsequent releases can be applied from the dashboard. Offline devices remain unchanged until they are online and explicitly updated.
|
|
55
|
-
|
|
56
|
-
## First run and trust
|
|
57
|
-
|
|
58
|
-
The official gateway base URL is `https://device.hcu-lab.me`. The official package pins the independently distributed application CA and requires protocol v2 before the first network connection. If that CA is absent or invalid, MCP Device fails closed instead of learning trust from the gateway it is about to contact or falling back to protocol v1.
|
|
59
|
-
|
|
60
|
-
For a custom gateway, set `MCP_GATEWAY_URL` and provision its application CA through an independent trusted channel before enabling protocol v2. Proxy selection is captured during interactive provisioning and reused by the background runtime.
|
|
61
|
-
|
|
62
|
-
## Local state and 1.0.2 migration
|
|
63
|
-
|
|
64
|
-
Canonical state is stored under `~/.mcp-device/`. Existing `~/.hcu-device/` identity/config/status state is migrated non-destructively under serialized runtime ownership. A conflicting canonical and legacy identity fails closed rather than creating a second device identity.
|
|
65
|
-
|
|
66
|
-
The device identity is Ed25519. MCP Device 1.0.2 does not use Windows DPAPI for the device private key or proxy configuration. A legacy protected 1.0.1 identity is archived without decrypting it, a fresh local identity is generated, and the device must be paired once again. A legacy protected proxy configuration must be supplied again during `mcp-device login`. The gateway stores only the public device identity and account ownership.
|
|
67
|
-
|
|
68
|
-
## Development
|
|
69
|
-
|
|
70
|
-
```powershell
|
|
71
|
-
git clone https://github.com/Hiroshimeow/agent-mcp-device.git
|
|
72
|
-
cd agent-mcp-device
|
|
73
|
-
npm ci
|
|
74
|
-
npm run build
|
|
75
|
-
node dist/mcp-device.js --help
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
Normal development pushes do not publish npm. After reviewed development work is committed on a clean `main`, a release is intentionally created with:
|
|
79
|
-
|
|
80
|
-
```powershell
|
|
81
|
-
npm run release
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
That command runs the test suite, bumps the patch version, synchronizes project version files, creates the release commit and `vX.Y.Z` tag, then atomically pushes `main` and the tag. The tag-triggered GitHub Actions workflow performs the npm publish through trusted OIDC publishing. Use `npm run release:minor` or `npm run release:major` only when that version change is intentional.
|
|
85
|
-
|
|
86
|
-
For a custom development gateway, set `MCP_GATEWAY_URL`. Supply `MCP_GATEWAY_APP_CA_PATH` from an independent trusted source when using protocol v2. Non-loopback plaintext gateway URLs are rejected.
|
|
87
|
-
|
|
88
|
-
## Upstream execution engine
|
|
89
|
-
|
|
90
|
-
The local execution engine is derived from [Desktop Commander MCP](https://github.com/wonderwhy-er/DesktopCommanderMCP). Desktop Commander is MIT licensed; its copyright and MIT license are preserved in this repository's `LICENSE`. The MCP Device product identity, gateway protocol, account/device lifecycle, security layer, and packaging are maintained separately in this repository.
|
package/dist/bootstrap.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* slow or cloud-synced filesystem — 4 stalled operations exhaust the pool, and
|
|
8
8
|
* because a stalled syscall keeps its thread until the OS returns (a JS-level
|
|
9
9
|
* timeout does not cancel it), every subsequent fs op queues for minutes. That
|
|
10
|
-
* surfaced as multi-minute tool-call hangs under parallel
|
|
10
|
+
* surfaced as multi-minute tool-call hangs under parallel agent load.
|
|
11
11
|
*
|
|
12
12
|
* Raising the pool size gives enough headroom that a burst of slow reads no
|
|
13
13
|
* longer starves the rest. libuv reads UV_THREADPOOL_SIZE only when the pool is
|
package/dist/config-manager.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
1
2
|
import fs from 'fs/promises';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import { existsSync } from 'fs';
|
|
@@ -49,29 +50,47 @@ class ConfigManager {
|
|
|
49
50
|
if (!existsSync(configDir)) {
|
|
50
51
|
await mkdir(configDir, { recursive: true });
|
|
51
52
|
}
|
|
52
|
-
|
|
53
|
+
let configData = null;
|
|
53
54
|
try {
|
|
54
|
-
await fs.
|
|
55
|
-
// Load existing config
|
|
56
|
-
const configData = await fs.readFile(this.configPath, 'utf8');
|
|
57
|
-
this.config = JSON.parse(configData);
|
|
58
|
-
this._isFirstRun = false;
|
|
59
|
-
// Configs created before this marker existed must not receive the
|
|
60
|
-
// welcome page retroactively when client eligibility changes later.
|
|
61
|
-
// New configs get this field from getDefaultConfig() and remain
|
|
62
|
-
// eligible across restarts until their first initialization.
|
|
63
|
-
if (this.config['welcomeOnboardingEligible'] === undefined) {
|
|
64
|
-
this.config['welcomeOnboardingEligible'] = false;
|
|
65
|
-
this.config['pendingWelcomeOnboarding'] = false;
|
|
66
|
-
await this.saveConfig();
|
|
67
|
-
}
|
|
55
|
+
configData = await fs.readFile(this.configPath, 'utf8');
|
|
68
56
|
}
|
|
69
57
|
catch (error) {
|
|
70
|
-
|
|
58
|
+
if (error?.code !== 'ENOENT')
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
if (configData === null) {
|
|
62
|
+
// Only a genuinely missing file creates and persists defaults.
|
|
71
63
|
this.config = this.getDefaultConfig();
|
|
72
|
-
this._isFirstRun = true;
|
|
64
|
+
this._isFirstRun = true;
|
|
73
65
|
await this.saveConfig();
|
|
74
66
|
}
|
|
67
|
+
else {
|
|
68
|
+
try {
|
|
69
|
+
this.config = JSON.parse(configData);
|
|
70
|
+
this._isFirstRun = false;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
// Preserve corrupt evidence rather than silently replacing user
|
|
74
|
+
// settings with defaults. The remote execution plane can continue
|
|
75
|
+
// safely with in-memory defaults.
|
|
76
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
77
|
+
const corruptPath = path.join(path.dirname(this.configPath), `config.corrupt-${stamp}.json`);
|
|
78
|
+
await fs.rename(this.configPath, corruptPath);
|
|
79
|
+
console.warn(`Invalid MCP Device config preserved at ${corruptPath}: ${error?.message || error}`);
|
|
80
|
+
this.config = this.getDefaultConfig();
|
|
81
|
+
this._isFirstRun = false;
|
|
82
|
+
}
|
|
83
|
+
// Legacy onboarding-marker migration is local-client state. Remote
|
|
84
|
+
// device children normalize it in memory only so parent/child startup
|
|
85
|
+
// does not create a second writer for config.json.
|
|
86
|
+
if (this.config['welcomeOnboardingEligible'] === undefined) {
|
|
87
|
+
this.config['welcomeOnboardingEligible'] = false;
|
|
88
|
+
this.config['pendingWelcomeOnboarding'] = false;
|
|
89
|
+
if (process.env.MCP_DEVICE_REMOTE !== 'true') {
|
|
90
|
+
await this.saveConfig();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
75
94
|
this.config['version'] = VERSION;
|
|
76
95
|
this.initialized = true;
|
|
77
96
|
}
|
|
@@ -139,7 +158,7 @@ class ConfigManager {
|
|
|
139
158
|
}
|
|
140
159
|
// Use user's actual shell from environment
|
|
141
160
|
// On macOS, default to zsh (default since Catalina) since process.env.SHELL
|
|
142
|
-
// may not be set when
|
|
161
|
+
// may not be set when launched by a desktop host
|
|
143
162
|
const fallbackShell = os.platform() === 'darwin' ? '/bin/zsh' : '/bin/sh';
|
|
144
163
|
const userShell = process.env.SHELL || fallbackShell;
|
|
145
164
|
// Return just the shell path - we'll handle login shell flag elsewhere
|
|
@@ -160,7 +179,25 @@ class ConfigManager {
|
|
|
160
179
|
* own independent fs.writeFile of the same path.
|
|
161
180
|
*/
|
|
162
181
|
async writeConfigToDisk() {
|
|
163
|
-
|
|
182
|
+
const directory = path.dirname(this.configPath);
|
|
183
|
+
await fs.mkdir(directory, { recursive: true });
|
|
184
|
+
const temporary = path.join(directory, `.${path.basename(this.configPath)}.${process.pid}.${Date.now()}.${randomBytes(6).toString('hex')}.tmp`);
|
|
185
|
+
try {
|
|
186
|
+
await fs.writeFile(temporary, JSON.stringify(this.config, null, 2), {
|
|
187
|
+
encoding: 'utf8',
|
|
188
|
+
mode: 0o600,
|
|
189
|
+
flag: 'wx'
|
|
190
|
+
});
|
|
191
|
+
await fs.rename(temporary, this.configPath);
|
|
192
|
+
try {
|
|
193
|
+
await fs.chmod(this.configPath, 0o600);
|
|
194
|
+
}
|
|
195
|
+
catch { /* Windows ACLs inherit from the user profile. */ }
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
await fs.rm(temporary, { force: true }).catch(() => { });
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
164
201
|
}
|
|
165
202
|
/**
|
|
166
203
|
* Awaitable save, serialized on writeChain. Use for explicit, user-driven
|
package/dist/config.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from 'path';
|
|
|
2
2
|
import os from 'os';
|
|
3
3
|
// MCP Device owns one canonical per-user state/config root.
|
|
4
4
|
export const USER_HOME = os.homedir();
|
|
5
|
-
export const CONFIG_DIR = path.resolve(process.env.MCP_DEVICE_CONFIG_DIR ||
|
|
5
|
+
export const CONFIG_DIR = path.resolve(process.env.MCP_DEVICE_CONFIG_DIR || path.join(USER_HOME, '.mcp-device'));
|
|
6
6
|
// Paths relative to the config directory.
|
|
7
7
|
export const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
8
8
|
export const TOOL_CALL_FILE = path.join(CONFIG_DIR, 'logs', 'tool-calls.log');
|
package/dist/custom-stdio.js
CHANGED
|
@@ -64,8 +64,7 @@ export class FilteredStdioServerTransport extends StdioServerTransport {
|
|
|
64
64
|
this.clientName = clientName.toLowerCase();
|
|
65
65
|
// Detect Cline and disable notifications
|
|
66
66
|
if (this.clientName.includes('cline') ||
|
|
67
|
-
this.clientName.includes('vscode')
|
|
68
|
-
this.clientName === 'claude-dev') {
|
|
67
|
+
this.clientName.includes('vscode')) {
|
|
69
68
|
this.disableNotifications = true;
|
|
70
69
|
process.stderr.write(`[INFO] MCP Device: Notifications disabled for ${clientName}\n`);
|
|
71
70
|
}
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": "2.0.0",
|
|
3
|
-
"description": "
|
|
3
|
+
"description": "MCP Device local onboarding prompts",
|
|
4
4
|
"prompts": [
|
|
5
5
|
{
|
|
6
6
|
"id": "onb2_01",
|
|
7
7
|
"title": "Organize my Downloads folder",
|
|
8
8
|
"description": "Clean up and organize your messy Downloads folder into relevant subfolders automatically.",
|
|
9
9
|
"prompt": "Let's organize your Downloads folder! \n\nFirst, let me check what we're working with. I'll look at your Downloads folder to see how many files are there and what types.\n\nShould I start by analyzing your Downloads folder?",
|
|
10
|
-
"categories": [
|
|
10
|
+
"categories": [
|
|
11
|
+
"onboarding"
|
|
12
|
+
],
|
|
11
13
|
"secondaryTag": "Quick Start",
|
|
12
14
|
"votes": 0,
|
|
13
15
|
"gaClicks": 0,
|
|
14
16
|
"icon": "FolderOpen",
|
|
15
|
-
"author": "
|
|
17
|
+
"author": "MCP Device",
|
|
16
18
|
"verified": true
|
|
17
19
|
},
|
|
18
20
|
{
|
|
@@ -20,11 +22,14 @@
|
|
|
20
22
|
"title": "Explain codebase or repository to me",
|
|
21
23
|
"description": "Analyze and explain any codebase - local project or GitHub repository - including architecture, dependencies, and how it works.",
|
|
22
24
|
"prompt": "I'll analyze and explain any codebase for you! 🔍\n\n**What should I analyze?**\n\n*Local project:* `~/work/my-project` (replace with your path)\n*GitHub repo:* `https://github.com/user/repo`\n\nI'll break down the architecture, dependencies, and how everything works together!",
|
|
23
|
-
"categories": [
|
|
25
|
+
"categories": [
|
|
26
|
+
"onboarding"
|
|
27
|
+
],
|
|
28
|
+
"secondaryTag": "Code Analysis",
|
|
24
29
|
"votes": 0,
|
|
25
30
|
"gaClicks": 0,
|
|
26
31
|
"icon": "Code",
|
|
27
|
-
"author": "
|
|
32
|
+
"author": "MCP Device",
|
|
28
33
|
"verified": true
|
|
29
34
|
},
|
|
30
35
|
{
|
|
@@ -32,12 +37,14 @@
|
|
|
32
37
|
"title": "Create organized knowledge/documents folder",
|
|
33
38
|
"description": "Set up a well-structured knowledge base or document organization system with templates and suggested categories.",
|
|
34
39
|
"prompt": "Let's create an organized knowledge base! 📚\n\n**Where should I set it up?**\n\n*I suggest: `~/Documents/Knowledge-Base` (replace with your path) or give me a different location.*\n\nI'll create a clean folder structure with templates and organize any existing documents you have!",
|
|
35
|
-
"categories": [
|
|
40
|
+
"categories": [
|
|
41
|
+
"onboarding"
|
|
42
|
+
],
|
|
36
43
|
"secondaryTag": "Quick Start",
|
|
37
44
|
"votes": 0,
|
|
38
45
|
"gaClicks": 0,
|
|
39
46
|
"icon": "BookOpen",
|
|
40
|
-
"author": "
|
|
47
|
+
"author": "MCP Device",
|
|
41
48
|
"verified": true
|
|
42
49
|
},
|
|
43
50
|
{
|
|
@@ -45,12 +52,14 @@
|
|
|
45
52
|
"title": "Analyze my data file",
|
|
46
53
|
"description": "Upload or point to any data file (CSV, JSON, Excel, etc.) and get comprehensive analysis including patterns, insights, and summary reports.",
|
|
47
54
|
"prompt": "I'll help you analyze your data file! \n\nWhat's the path to your data file? (e.g., `/Users/yourname/data.csv`)\n\nOnce you give me the path, I'll start by checking what type of file it is and show you a quick preview, then we can dive deeper step by step.",
|
|
48
|
-
"categories": [
|
|
55
|
+
"categories": [
|
|
56
|
+
"onboarding"
|
|
57
|
+
],
|
|
49
58
|
"secondaryTag": "Quick Start",
|
|
50
59
|
"votes": 0,
|
|
51
60
|
"gaClicks": 0,
|
|
52
61
|
"icon": "BarChart3",
|
|
53
|
-
"author": "
|
|
62
|
+
"author": "MCP Device",
|
|
54
63
|
"verified": true
|
|
55
64
|
},
|
|
56
65
|
{
|
|
@@ -58,12 +67,14 @@
|
|
|
58
67
|
"title": "Check system health and resources",
|
|
59
68
|
"description": "Analyze your system's health, resource usage, running processes, and generate a comprehensive system status report.",
|
|
60
69
|
"prompt": "Let me check your system health and resources!\n\nI'll start by looking at your CPU, memory, and disk usage, then check for any performance issues.\n\nShould I begin the system analysis?",
|
|
61
|
-
"categories": [
|
|
70
|
+
"categories": [
|
|
71
|
+
"onboarding"
|
|
72
|
+
],
|
|
62
73
|
"secondaryTag": "Quick Start",
|
|
63
74
|
"votes": 0,
|
|
64
75
|
"gaClicks": 0,
|
|
65
76
|
"icon": "Activity",
|
|
66
|
-
"author": "
|
|
77
|
+
"author": "MCP Device",
|
|
67
78
|
"verified": true
|
|
68
79
|
}
|
|
69
80
|
]
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
export declare const DEVICE_STATE_FILES: readonly string[];
|
|
2
2
|
export interface DeviceStatePaths {
|
|
3
3
|
root: string;
|
|
4
|
-
legacyRoot: string;
|
|
5
4
|
identity: string;
|
|
6
5
|
config: string;
|
|
7
6
|
status: string;
|
|
7
|
+
runtime: string;
|
|
8
|
+
update: string;
|
|
8
9
|
}
|
|
9
10
|
export declare function deviceStatePaths(home?: string): DeviceStatePaths;
|
|
10
|
-
export declare function migrateLegacyDeviceState(paths?: DeviceStatePaths): Promise<{
|
|
11
|
-
migrated: string[];
|
|
12
|
-
legacyStateSeen: boolean;
|
|
13
|
-
}>;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
1
|
import os from 'os';
|
|
3
2
|
import path from 'path';
|
|
4
3
|
export const DEVICE_STATE_FILES = Object.freeze([
|
|
@@ -8,93 +7,12 @@ export const DEVICE_STATE_FILES = Object.freeze([
|
|
|
8
7
|
]);
|
|
9
8
|
export function deviceStatePaths(home = os.homedir()) {
|
|
10
9
|
const root = path.resolve(home, '.mcp-device');
|
|
11
|
-
const legacyRoot = path.resolve(home, '.hcu-device');
|
|
12
10
|
return {
|
|
13
11
|
root,
|
|
14
|
-
legacyRoot,
|
|
15
12
|
identity: path.join(root, 'gateway-identity.json'),
|
|
16
13
|
config: path.join(root, 'gateway-config.json'),
|
|
17
|
-
status: path.join(root, 'gateway-status.json')
|
|
14
|
+
status: path.join(root, 'gateway-status.json'),
|
|
15
|
+
runtime: path.join(root, 'runtime'),
|
|
16
|
+
update: path.join(root, 'update')
|
|
18
17
|
};
|
|
19
18
|
}
|
|
20
|
-
async function readJsonIfExists(filePath) {
|
|
21
|
-
try {
|
|
22
|
-
return JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
23
|
-
}
|
|
24
|
-
catch (error) {
|
|
25
|
-
if (error?.code === 'ENOENT')
|
|
26
|
-
return null;
|
|
27
|
-
throw new Error(`Invalid MCP Device state file ${filePath}: ${error?.message || error}`);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
async function copyAtomic(source, target) {
|
|
31
|
-
const content = await fs.readFile(source);
|
|
32
|
-
await fs.mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
33
|
-
const temporary = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
34
|
-
await fs.writeFile(temporary, content, { mode: 0o600, flag: 'wx' });
|
|
35
|
-
await fs.rename(temporary, target);
|
|
36
|
-
try {
|
|
37
|
-
await fs.chmod(target, 0o600);
|
|
38
|
-
}
|
|
39
|
-
catch { /* Windows ACLs inherit from the user profile. */ }
|
|
40
|
-
}
|
|
41
|
-
function identityFingerprint(value) {
|
|
42
|
-
if (!value)
|
|
43
|
-
return null;
|
|
44
|
-
const deviceId = String(value.deviceId || '').trim();
|
|
45
|
-
const publicKeyPem = String(value.publicKeyPem || '').trim();
|
|
46
|
-
return deviceId && publicKeyPem ? `${deviceId}\n${publicKeyPem}` : null;
|
|
47
|
-
}
|
|
48
|
-
export async function migrateLegacyDeviceState(paths = deviceStatePaths()) {
|
|
49
|
-
await fs.mkdir(paths.root, { recursive: true, mode: 0o700 });
|
|
50
|
-
const legacyIdentityPath = path.join(paths.legacyRoot, 'gateway-identity.json');
|
|
51
|
-
const canonicalIdentity = await readJsonIfExists(paths.identity);
|
|
52
|
-
const legacyIdentity = await readJsonIfExists(legacyIdentityPath);
|
|
53
|
-
const legacyPresence = await Promise.all(DEVICE_STATE_FILES.map(async (name) => {
|
|
54
|
-
try {
|
|
55
|
-
await fs.access(path.join(paths.legacyRoot, name));
|
|
56
|
-
return true;
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
}));
|
|
62
|
-
const legacyStateSeen = legacyPresence.some(Boolean);
|
|
63
|
-
if (canonicalIdentity && legacyIdentity) {
|
|
64
|
-
const canonicalFingerprint = identityFingerprint(canonicalIdentity);
|
|
65
|
-
const legacyFingerprint = identityFingerprint(legacyIdentity);
|
|
66
|
-
if (!canonicalFingerprint || !legacyFingerprint || canonicalFingerprint !== legacyFingerprint) {
|
|
67
|
-
throw new Error('MCP Device migration conflict: canonical and legacy device identities differ.');
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
if (legacyStateSeen && !legacyIdentity && !canonicalIdentity) {
|
|
71
|
-
throw new Error('MCP Device migration failed closed: legacy state exists without a device identity.');
|
|
72
|
-
}
|
|
73
|
-
const migrated = [];
|
|
74
|
-
for (const name of DEVICE_STATE_FILES) {
|
|
75
|
-
const source = path.join(paths.legacyRoot, name);
|
|
76
|
-
const target = path.join(paths.root, name);
|
|
77
|
-
let sourceExists = true;
|
|
78
|
-
try {
|
|
79
|
-
await fs.access(source);
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
sourceExists = false;
|
|
83
|
-
}
|
|
84
|
-
if (!sourceExists)
|
|
85
|
-
continue;
|
|
86
|
-
await readJsonIfExists(source);
|
|
87
|
-
let targetExists = true;
|
|
88
|
-
try {
|
|
89
|
-
await fs.access(target);
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
targetExists = false;
|
|
93
|
-
}
|
|
94
|
-
if (!targetExists) {
|
|
95
|
-
await copyAtomic(source, target);
|
|
96
|
-
migrated.push(name);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return { migrated, legacyStateSeen };
|
|
100
|
-
}
|
package/dist/device/device.d.ts
CHANGED
package/dist/device/device.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { fileURLToPath } from 'url';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import {
|
|
5
|
+
import { LocalExecutionEngine } from './execution-engine.js';
|
|
6
6
|
import { GatewayDeviceChannel } from './gateway-channel.js';
|
|
7
7
|
import { GatewayDeviceConfigStore, createGatewayProxyAgent } from './gateway-config.js';
|
|
8
8
|
import { GatewayDeviceIdentity } from './gateway-identity.js';
|
|
@@ -11,12 +11,13 @@ import { bootstrapOfficialGatewayTrust } from './official-trust.js';
|
|
|
11
11
|
import { GatewayDeviceStatusStore } from './device-status.js';
|
|
12
12
|
import { GatewayToolAdapter } from './gateway-tool-adapter.js';
|
|
13
13
|
import { RuntimeOwner, acquireRuntimeOwner } from './runtime-owner.js';
|
|
14
|
-
import {
|
|
14
|
+
import { assertDeviceStartupAllowedDuringUpdate, launchDeviceUpdateHelper, prepareDevicePackageUpdate, reconcileDeviceUpdateAfterStart, retireDeviceUpdateState, signalDeviceUpdateHandoff } from './self-update.js';
|
|
15
15
|
import { captureRemote } from '../utils/capture.js';
|
|
16
|
+
import { VERSION } from '../version.js';
|
|
16
17
|
export class MCPDevice {
|
|
17
18
|
constructor() {
|
|
18
19
|
this.isShuttingDown = false;
|
|
19
|
-
this.
|
|
20
|
+
this.executionEngine = new LocalExecutionEngine();
|
|
20
21
|
this.setupShutdownHandlers();
|
|
21
22
|
}
|
|
22
23
|
setupShutdownHandlers() {
|
|
@@ -62,6 +63,9 @@ export class MCPDevice {
|
|
|
62
63
|
console.log('🚀 Starting MCP Device...');
|
|
63
64
|
if (process.env.DEBUG_MODE === 'true')
|
|
64
65
|
console.log(' - 🐞 DEBUG_MODE');
|
|
66
|
+
// A fresh external update helper owns the package lifecycle.
|
|
67
|
+
// Refuse a competing runtime until it completes or becomes stale.
|
|
68
|
+
await assertDeviceStartupAllowedDuringUpdate(VERSION);
|
|
65
69
|
const managerArg = process.argv.find(value => value.startsWith('--manager='));
|
|
66
70
|
const manager = managerArg?.slice('--manager='.length);
|
|
67
71
|
const mode = process.argv.includes('--service')
|
|
@@ -71,7 +75,7 @@ export class MCPDevice {
|
|
|
71
75
|
await acquireRuntimeOwner(this.runtimeOwner, { confirmTakeover: options.confirmTakeover });
|
|
72
76
|
await this.runtimeOwner.bootstrap();
|
|
73
77
|
await bootstrapOfficialGatewayTrust(new GatewayDeviceConfigStore());
|
|
74
|
-
await this.
|
|
78
|
+
await this.executionEngine.initialize();
|
|
75
79
|
const gatewayStatus = new GatewayDeviceStatusStore();
|
|
76
80
|
const storedGatewayStatus = await gatewayStatus.load();
|
|
77
81
|
const gatewayConfig = await new GatewayDeviceConfigStore().load();
|
|
@@ -111,12 +115,40 @@ export class MCPDevice {
|
|
|
111
115
|
proxyAgent: proxy.agent,
|
|
112
116
|
securityProtocolFloor: gatewayConfig.securityProtocolFloor,
|
|
113
117
|
appCaPem: gatewayConfig.appCaPem || undefined,
|
|
114
|
-
adapter: new GatewayToolAdapter(this.
|
|
118
|
+
adapter: new GatewayToolAdapter(this.executionEngine, { allowedRoots: gatewayConfig.allowedRoots }),
|
|
119
|
+
runtimeState: () => this.executionEngine.getRuntimeState(),
|
|
115
120
|
agentVersion: process.env.npm_package_version,
|
|
116
|
-
onUpdateRequest: async (targetVersion) => {
|
|
121
|
+
onUpdateRequest: async (targetVersion, { requestId }) => {
|
|
117
122
|
const record = await identity.loadOrCreate();
|
|
118
|
-
await
|
|
119
|
-
|
|
123
|
+
const prepared = await prepareDevicePackageUpdate({
|
|
124
|
+
targetVersion,
|
|
125
|
+
requestId,
|
|
126
|
+
deviceId: record.deviceId,
|
|
127
|
+
childPid: this.executionEngine.getChildPid(),
|
|
128
|
+
fromVersion: VERSION
|
|
129
|
+
});
|
|
130
|
+
try {
|
|
131
|
+
// Launch the helper while the service is healthy. The
|
|
132
|
+
// helper remains inert until the explicit handoff marker.
|
|
133
|
+
await launchDeviceUpdateHelper(prepared);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
await retireDeviceUpdateState(prepared.state).catch(() => { });
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
mode: 'detached',
|
|
141
|
+
handoff: async () => {
|
|
142
|
+
// Capture the last live child PID, then complete graceful
|
|
143
|
+
// shutdown and RuntimeOwner release before arming the helper.
|
|
144
|
+
// This preserves G5.6: no destructive update work may begin
|
|
145
|
+
// until the old runtime has finished releasing package handles.
|
|
146
|
+
const childPid = this.executionEngine.getChildPid();
|
|
147
|
+
await this.shutdown();
|
|
148
|
+
await signalDeviceUpdateHandoff({ childPid });
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
120
152
|
},
|
|
121
153
|
onStatus: async (payload) => {
|
|
122
154
|
const record = await identity.loadOrCreate();
|
|
@@ -138,6 +170,34 @@ export class MCPDevice {
|
|
|
138
170
|
}
|
|
139
171
|
});
|
|
140
172
|
await this.gatewayChannel.start();
|
|
173
|
+
const updateReconciliation = await reconcileDeviceUpdateAfterStart(VERSION);
|
|
174
|
+
if (updateReconciliation.action === 'success') {
|
|
175
|
+
// Successful reconnect already advertised the target package_version
|
|
176
|
+
// in auth_hello; that reconnect is the authoritative completion proof.
|
|
177
|
+
// Do not send a late "installed" status: the gateway completes the
|
|
178
|
+
// update from the authenticated reconnect itself.
|
|
179
|
+
try {
|
|
180
|
+
await retireDeviceUpdateState(updateReconciliation.state);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
// A transient Windows EPERM/EBUSY while the detached helper is
|
|
184
|
+
// exiting must not turn a healthy target reconnect into startup failure.
|
|
185
|
+
console.warn('Update cleanup deferred:', error?.message || error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else if (updateReconciliation.action === 'report-failed') {
|
|
189
|
+
try {
|
|
190
|
+
await this.gatewayChannel.reportUpdateFailure(updateReconciliation.state.requestId, updateReconciliation.state.targetVersion, updateReconciliation.state.errorCode || 'DEVICE_UPDATE_FAILED', updateReconciliation.state.message || 'MCP Device update rolled back.');
|
|
191
|
+
await retireDeviceUpdateState(updateReconciliation.state).catch((error) => {
|
|
192
|
+
console.warn('Failed update cleanup deferred:', error?.message || error);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
// Keep durable failed state for a later reporting attempt; do not
|
|
197
|
+
// make the restored runtime unavailable because reporting raced.
|
|
198
|
+
console.warn('Failed update report deferred:', error?.message || error);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
141
201
|
console.log('✓ Device ready through direct Gateway channel');
|
|
142
202
|
}
|
|
143
203
|
catch (error) {
|
|
@@ -165,7 +225,7 @@ export class MCPDevice {
|
|
|
165
225
|
this.gatewayProxyAgent = undefined;
|
|
166
226
|
}
|
|
167
227
|
await new GatewayDeviceStatusStore().markOffline().catch(() => { });
|
|
168
|
-
await this.
|
|
228
|
+
await this.executionEngine.shutdown();
|
|
169
229
|
if (this.runtimeOwner) {
|
|
170
230
|
await this.runtimeOwner.release().catch(() => { });
|
|
171
231
|
this.runtimeOwner = undefined;
|
|
@@ -4,12 +4,22 @@ interface McpConfig {
|
|
|
4
4
|
cwd?: string;
|
|
5
5
|
env?: Record<string, string>;
|
|
6
6
|
}
|
|
7
|
-
export declare class
|
|
7
|
+
export declare class LocalExecutionEngine {
|
|
8
8
|
private mcpClient;
|
|
9
9
|
private mcpTransport;
|
|
10
10
|
private isReady;
|
|
11
|
+
private runtimeReason;
|
|
12
|
+
private runtimeGeneration;
|
|
13
|
+
private markUnavailable;
|
|
11
14
|
initialize(): Promise<void>;
|
|
12
15
|
resolveMcpConfig(): Promise<McpConfig | null>;
|
|
16
|
+
getChildPid(): number | null;
|
|
17
|
+
getRuntimeState(): {
|
|
18
|
+
runtime_ready: boolean;
|
|
19
|
+
runtime_reason: string | null;
|
|
20
|
+
execution_runtime_generation: string | null;
|
|
21
|
+
};
|
|
22
|
+
assertReady(): void;
|
|
13
23
|
callClientTool(toolName: string, args: any, metadata?: any): Promise<{
|
|
14
24
|
[x: string]: unknown;
|
|
15
25
|
content: ({
|