@vo1d7bin/remote-mcp-agent 0.0.0-stage → 0.1.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/README.md +42 -3
- package/bin/vo1d-agent.mjs +112 -0
- package/package.json +32 -6
package/README.md
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# vo1d Remote MCP device agent
|
|
2
|
+
|
|
3
|
+
Independent, open CLI for connecting an authorized Windows, Linux or macOS device to **your own** account at https://mcp.thelinuxserver.cloud. This is **not** the third-party Desktop Commander relay.
|
|
4
|
+
|
|
5
|
+
Requires Node.js 22.14 or newer.
|
|
6
|
+
|
|
7
|
+
### Pair this machine
|
|
8
|
+
|
|
9
|
+
Sign in to https://mcp.thelinuxserver.cloud/dashboard and choose **Pair a device**. Copy the one-time code. Run:
|
|
10
|
+
|
|
11
|
+
```powershell
|
|
12
|
+
npx -y @vo1d7bin/remote-mcp-agent@latest enroll
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Paste the code when prompted. The credentials are saved outside your source tree at `~/.vo1d-remote-mcp/device.json`. Never share this file.
|
|
16
|
+
|
|
17
|
+
### Start the agent
|
|
18
|
+
|
|
19
|
+
```powershell
|
|
20
|
+
npx -y @vo1d7bin/remote-mcp-agent@latest remote --allow-root "C:\Users\Public\Documents"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For Linux: `npx -y @vo1d7bin/remote-mcp-agent@latest remote --allow-root /home/user/mcp-share`.
|
|
24
|
+
|
|
25
|
+
Use `VO1D_ALLOWED_ROOTS` to set multiple allowed paths, separated by your system PATH delimiter (semicolon on Windows, colon on Linux). Without allowed roots the agent only returns basic system information. An allowed root may expose file and directory names within that folder to the account's explicitly approved MCP clients. Only select directories you're comfortable exposing.
|
|
26
|
+
|
|
27
|
+
### Useful options
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
enroll [--server https://mcp.thelinuxserver.cloud] [--name YOUR-PC]
|
|
31
|
+
remote [--allow-root PATH]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For a second agent profile, set `VO1D_CONFIG_DIR` before enrollment and when starting its `remote` process.
|
|
35
|
+
|
|
36
|
+
The initial release supports only `system_info` and `list_directory`. It cannot read file contents, write files, or run shell commands. It reconnects automatically while its process is running. To revoke credentials, open the vo1d Devices dashboard and select **Revoke**.
|
|
37
|
+
|
|
38
|
+
### Security
|
|
39
|
+
|
|
40
|
+
Only install this package from the official npm registry under the `@vo1d7bin` scope. Do not run agents as Administrator/root unless necessary. Do not forward or publish the device token. Use explicit, limited allowlisted roots and review client permissions.
|
|
41
|
+
|
|
42
|
+
Maintained source: https://github.com/THE-L1NUX-USER/vo1d-remote-mcp/tree/main/agent
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import WebSocket from 'ws';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { promises as fs } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import readline from 'node:readline/promises';
|
|
7
|
+
|
|
8
|
+
const folder = process.env.VO1D_CONFIG_DIR ? path.resolve(process.env.VO1D_CONFIG_DIR) : path.join(os.homedir(), '.vo1d-remote-mcp');
|
|
9
|
+
const configFile = path.join(folder, 'device.json');
|
|
10
|
+
const arg = (name) => { const i = process.argv.indexOf(name); return i < 0 ? undefined : process.argv[i + 1]; };
|
|
11
|
+
function normalizedServer(raw) {
|
|
12
|
+
const u = new URL(raw);
|
|
13
|
+
if (u.protocol !== 'https:' && !(u.protocol === 'http:' &&
|
|
14
|
+
['127.0.0.1', 'localhost'].includes(u.hostname))) {
|
|
15
|
+
throw new Error('An HTTPS server URL is required outside localhost');
|
|
16
|
+
}
|
|
17
|
+
return u.origin;
|
|
18
|
+
}
|
|
19
|
+
async function enroll() {
|
|
20
|
+
const server = normalizedServer(arg('--server') || process.env.VO1D_SERVER || 'https://mcp.thelinuxserver.cloud');
|
|
21
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
22
|
+
let code;
|
|
23
|
+
try { code = (await rl.question('Paste the one-time pairing code: ')).trim(); }
|
|
24
|
+
finally { rl.close(); }
|
|
25
|
+
const res = await fetch(server + '/api/agent/enroll', {
|
|
26
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
27
|
+
body: JSON.stringify({ code, name: arg('--name') || os.hostname(), platform: process.platform })
|
|
28
|
+
});
|
|
29
|
+
if (!res.ok) throw new Error('Device pairing failed (HTTP ' + res.status + ')');
|
|
30
|
+
const { id, token } = await res.json();
|
|
31
|
+
await fs.mkdir(folder, { recursive: true, mode: 0o700 });
|
|
32
|
+
try {
|
|
33
|
+
await fs.writeFile(configFile, JSON.stringify({ server, id, token }, null, 2), { mode: 0o600, flag: 'wx' });
|
|
34
|
+
} catch(error) {
|
|
35
|
+
if (error.code === 'EEXIST') {
|
|
36
|
+
throw new Error('A device is already enrolled in this profile. Run "remote" or specify VO1D_CONFIG_DIR to use a separate profile.');
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
console.log('Paired successfully. Device ID:', id);
|
|
41
|
+
console.log('Credentials saved outside the Git repository:', configFile);
|
|
42
|
+
console.log('Run: node agent/index.mjs remote');
|
|
43
|
+
}
|
|
44
|
+
async function allowedRoots() {
|
|
45
|
+
const provided = process.env.VO1D_ALLOWED_ROOTS || arg('--allow-root') || '';
|
|
46
|
+
const paths = provided.split(path.delimiter).map(s => s.trim()).filter(Boolean);
|
|
47
|
+
const roots = [];
|
|
48
|
+
for (const p of paths) roots.push(await fs.realpath(path.resolve(p)));
|
|
49
|
+
return roots;
|
|
50
|
+
}
|
|
51
|
+
function includesRoot(p, root) {
|
|
52
|
+
const value = process.platform === 'win32' ? p.toLowerCase() : p;
|
|
53
|
+
const base = process.platform === 'win32' ? root.toLowerCase() : root;
|
|
54
|
+
return value === base || value.startsWith(base.endsWith(path.sep) ? base : base + path.sep);
|
|
55
|
+
}
|
|
56
|
+
async function directoryListing(requestPath, roots) {
|
|
57
|
+
if (!roots.length) throw new Error('No allowed roots configured; set VO1D_ALLOWED_ROOTS');
|
|
58
|
+
if (typeof requestPath !== 'string' || requestPath.length > 2000) throw new Error('Invalid directory path');
|
|
59
|
+
const resolved = await fs.realpath(path.resolve(requestPath));
|
|
60
|
+
if (!roots.some(root => includesRoot(resolved, root))) throw new Error('Path outside allowed directories');
|
|
61
|
+
const entries = await fs.readdir(resolved, { withFileTypes: true });
|
|
62
|
+
return { path: resolved, truncated: entries.length > 100, items: entries.slice(0, 100)
|
|
63
|
+
.map(entry => ({ name: entry.name, type: entry.isDirectory() ? 'directory' : entry.isFile() ? 'file' : 'other' })) };
|
|
64
|
+
}
|
|
65
|
+
async function remote() {
|
|
66
|
+
const cfg = JSON.parse(await fs.readFile(configFile, 'utf8'));
|
|
67
|
+
const server = normalizedServer(cfg.server);
|
|
68
|
+
const roots = await allowedRoots();
|
|
69
|
+
console.log('Device:', cfg.id, '| Allowed filesystem roots:', roots.length ? roots.join(' ; ') : '(none)');
|
|
70
|
+
const endpoint = server.replace(/^http/, 'ws') + '/agent';
|
|
71
|
+
let stopped = false;
|
|
72
|
+
let retry = 1000;
|
|
73
|
+
process.on('SIGINT', () => { stopped = true; process.exit(0); });
|
|
74
|
+
async function connect() {
|
|
75
|
+
if (stopped) return;
|
|
76
|
+
const ws = new WebSocket(endpoint, { headers: { Authorization: 'Bearer ' + cfg.token }, handshakeTimeout: 12000 });
|
|
77
|
+
ws.on('open', () => { retry = 1000; console.log('Connected to', server); });
|
|
78
|
+
ws.on('message', async raw => {
|
|
79
|
+
let task;
|
|
80
|
+
try { task = JSON.parse(raw.toString()); } catch { return; }
|
|
81
|
+
if (typeof task.id !== 'string') return;
|
|
82
|
+
let reply;
|
|
83
|
+
try {
|
|
84
|
+
if (task.action === 'system_info') {
|
|
85
|
+
reply = { hostname: os.hostname(), platform: process.platform, arch: process.arch,
|
|
86
|
+
release: os.release(), cpus: os.cpus().length, memoryBytes: os.totalmem(), allowedRoots: roots };
|
|
87
|
+
} else if (task.action === 'list_directory') {
|
|
88
|
+
reply = await directoryListing(task.args?.path, roots);
|
|
89
|
+
} else {
|
|
90
|
+
throw new Error('Unsupported action');
|
|
91
|
+
}
|
|
92
|
+
ws.send(JSON.stringify({ id: task.id, ok: true, result: reply }));
|
|
93
|
+
} catch (error) {
|
|
94
|
+
ws.send(JSON.stringify({ id: task.id, ok: false, error: String(error.message).slice(0, 150) }));
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
ws.on('error', error => console.error('Connection:', error.message));
|
|
98
|
+
ws.on('close', (code) => {
|
|
99
|
+
if (code === 4001) { console.error('Device revoked. Exiting.'); stopped = true; return; }
|
|
100
|
+
if (stopped) return;
|
|
101
|
+
const delay = retry;
|
|
102
|
+
retry = Math.min(retry * 2, 30000);
|
|
103
|
+
console.log('Disconnected. Retrying in', delay / 1000, 'seconds.');
|
|
104
|
+
setTimeout(connect, delay);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
await connect();
|
|
108
|
+
}
|
|
109
|
+
const command = process.argv[2];
|
|
110
|
+
if (command === 'enroll') enroll().catch(e => { console.error(e.message); process.exitCode = 1; });
|
|
111
|
+
else if (command === 'remote') remote().catch(e => { console.error(e.message); process.exitCode = 1; });
|
|
112
|
+
else console.log('Usage: vo1d-agent enroll [--server https://mcp.thelinuxserver.cloud] [--name MY-PC]\n vo1d-agent remote [--allow-root PATH]\nPass VO1D_ALLOWED_ROOTS for multiple roots or VO1D_CONFIG_DIR for separate profiles.');
|
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@vo1d7bin/remote-mcp-agent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@vo1d7bin/remote-mcp-agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Independent vo1d Remote MCP device agent for Windows, Linux and macOS",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/THE-L1NUX-USER/vo1d-remote-mcp.git",
|
|
10
|
+
"directory": "packages/agent"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://mcp.thelinuxserver.cloud",
|
|
13
|
+
"bin": {
|
|
14
|
+
"vo1d-agent": "bin/vo1d-agent.mjs"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"bin/",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=22.14.0"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"ws": "8.18.3"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"prepack": "node ../../scripts/sync-agent.mjs"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
}
|
|
32
|
+
}
|