@vo1d7bin/remote-mcp-agent 0.2.0 → 0.3.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 +41 -17
- package/bin/cli-ux.mjs +70 -0
- package/bin/management.mjs +104 -0
- package/bin/vo1d-agent.mjs +29 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,42 +1,66 @@
|
|
|
1
1
|
# vo1d Remote MCP device agent
|
|
2
2
|
|
|
3
|
-
Connect
|
|
3
|
+
Connect Windows, Linux or macOS to your account at **https://mcp.thelinuxserver.cloud**. Independent of Desktop Commander.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Node.js 22.14+ required.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Connect in one command
|
|
8
8
|
|
|
9
9
|
```powershell
|
|
10
10
|
npx -y @vo1d7bin/remote-mcp-agent@latest remote
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
First run opens a browser to approve your device through your existing Google account. Compare the verification code in the browser and terminal, then approve. The agent stores its credentials under your user profile and connects automatically. Subsequent runs restore that authorization; you do not need to pair every time.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
A compact connection screen confirms the device name, account, unique device ID and connection status, with a short reminder to keep the terminal open.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
### Commands
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
| Command | Purpose |
|
|
20
|
+
| --- | --- |
|
|
21
|
+
| `remote` | Connect, or authorize in browser the first time |
|
|
22
|
+
| `status` | Check the server-recorded online/offline state, account and device ID |
|
|
23
|
+
| `logout` | Revoke this device in the server and remove saved local credentials |
|
|
24
|
+
| `open` | Open the dashboard in your browser |
|
|
25
|
+
| `help` / `--help` | Show available commands and examples |
|
|
26
|
+
| `--version` | Display the agent version |
|
|
27
|
+
| `enroll` | Manual code-based pairing for advanced/headless setups |
|
|
28
|
+
|
|
29
|
+
Use the same `npx -y @vo1d7bin/remote-mcp-agent@latest` prefix for each command. For example:
|
|
20
30
|
|
|
21
31
|
```powershell
|
|
22
|
-
npx -y @vo1d7bin/remote-mcp-agent@latest
|
|
32
|
+
npx -y @vo1d7bin/remote-mcp-agent@latest status
|
|
33
|
+
npx -y @vo1d7bin/remote-mcp-agent@latest open
|
|
34
|
+
npx -y @vo1d7bin/remote-mcp-agent@latest logout
|
|
23
35
|
```
|
|
24
36
|
|
|
25
|
-
|
|
37
|
+
`remote --help` and `remote --logout` are accepted as convenience aliases.
|
|
26
38
|
|
|
27
|
-
|
|
39
|
+
**Logout revokes the device credential on the server**, disconnecting a running agent. For safety, if the relay is unreachable, the CLI preserves local credentials and tells you how to revoke through the dashboard. `logout --local` is a last-resort local cleanup only: it does **not** revoke server authorization, and you must also revoke the device in the dashboard.
|
|
40
|
+
|
|
41
|
+
## Share a specific folder (optional)
|
|
42
|
+
|
|
43
|
+
By default, the agent exposes basic system information only. To allow a client to list names in a safe directory:
|
|
28
44
|
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
```powershell
|
|
46
|
+
mkdir "$HOME\mcp-share" -Force
|
|
47
|
+
npx -y @vo1d7bin/remote-mcp-agent@latest remote --allow-root "$HOME\mcp-share"
|
|
32
48
|
```
|
|
33
49
|
|
|
34
|
-
|
|
50
|
+
Never use a secrets directory or the entire system drive as an allowed root. File contents, file modifications and terminal commands are **not** enabled in this version. The public MCP endpoint also remains gated until the end-to-end OAuth integration is ready.
|
|
35
51
|
|
|
36
|
-
|
|
52
|
+
The process needs to keep running to remain online. Closing the terminal makes the device offline but does not log it out.
|
|
53
|
+
|
|
54
|
+
## Advanced options
|
|
37
55
|
|
|
38
|
-
|
|
56
|
+
- `--server URL`: use a custom HTTPS server (local HTTP development is allowed only for localhost)
|
|
57
|
+
- `--name NAME`: set a name during first-time authorization
|
|
58
|
+
- `--no-browser`: print the verification link for another computer instead of attempting to open a local browser
|
|
59
|
+
- `--allow-root PATH`: allow directory listing inside the specified safe path
|
|
60
|
+
- `VO1D_ALLOWED_ROOTS`: additional allowed paths separated by the system path delimiter
|
|
61
|
+
- `VO1D_CONFIG_DIR`: isolate a second authorized device profile
|
|
62
|
+
- `NO_COLOR=1`: suppress terminal ANSI colors
|
|
39
63
|
|
|
40
|
-
|
|
64
|
+
Future installers can run the same background agent without requiring users to install Node themselves.
|
|
41
65
|
|
|
42
66
|
Source: https://github.com/THE-L1NUX-USER/vo1d-remote-mcp
|
package/bin/cli-ux.mjs
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const colors=Boolean(process.stdout.isTTY && !process.env.NO_COLOR && process.env.TERM!=='dumb');
|
|
2
|
+
const tint=(s,c)=>colors?'['+c+'m'+s+'[0m':s;
|
|
3
|
+
const cyan=s=>tint(s,'96'), green=s=>tint(s,'92'), dim=s=>tint(s,'90');
|
|
4
|
+
export const version='0.3.0';
|
|
5
|
+
export function banner() {
|
|
6
|
+
console.log('');
|
|
7
|
+
console.log(cyan(' __ __ ___ _ ____'));
|
|
8
|
+
console.log(cyan(' \\ \\ / / / _ \\ / | | _ \\'));
|
|
9
|
+
console.log(cyan(' \\ V / | (_) || | | | | |'));
|
|
10
|
+
console.log(cyan(' \\_/ \\___/ |_| |____/'));
|
|
11
|
+
console.log('');
|
|
12
|
+
console.log(' '+cyan('REMOTE MCP')+dim(' · v'+version));
|
|
13
|
+
console.log(' '+dim('Your machines. Your control.'));
|
|
14
|
+
console.log('');
|
|
15
|
+
}
|
|
16
|
+
export function heading(title) {console.log(cyan('◆ '+title));}
|
|
17
|
+
export function info(label,value){console.log(' '+dim(label.padEnd(15))+String(value||'—'));}
|
|
18
|
+
export function success(message){console.log(green(' ✓ '+message));}
|
|
19
|
+
export function note(message){console.log(dim(' '+message));}
|
|
20
|
+
export function divider(){console.log(dim(' ───────────────────────────────────────────────────'));}
|
|
21
|
+
export function help() {
|
|
22
|
+
banner();
|
|
23
|
+
heading('Commands');
|
|
24
|
+
console.log(' remote Connect this device (default; browser approval on first run)');
|
|
25
|
+
console.log(' status Show this device, account and current relay status');
|
|
26
|
+
console.log(' logout Revoke device authorization and delete local credentials');
|
|
27
|
+
console.log(' open Open the online device dashboard in your browser');
|
|
28
|
+
console.log(' enroll Manual pairing code for headless or advanced setups');
|
|
29
|
+
console.log(' help Show these commands');
|
|
30
|
+
console.log(' --version Show CLI version');
|
|
31
|
+
console.log('');
|
|
32
|
+
heading('Useful options');
|
|
33
|
+
console.log(' remote --allow-root PATH Share directory names from an approved folder');
|
|
34
|
+
console.log(' remote --no-browser Print approval link instead of opening it');
|
|
35
|
+
console.log(' remote --server URL Use a custom vo1d server');
|
|
36
|
+
console.log(' logout --local Forget credentials without remote revocation');
|
|
37
|
+
console.log(' --name DEVICE_NAME Choose name when first pairing');
|
|
38
|
+
console.log('');
|
|
39
|
+
heading('Examples');
|
|
40
|
+
console.log(' npx -y @vo1d7bin/remote-mcp-agent@latest remote');
|
|
41
|
+
console.log(' npx -y @vo1d7bin/remote-mcp-agent@latest remote --allow-root ./mcp-share');
|
|
42
|
+
console.log(' npx -y @vo1d7bin/remote-mcp-agent@latest status');
|
|
43
|
+
console.log(' npx -y @vo1d7bin/remote-mcp-agent@latest logout');
|
|
44
|
+
console.log('');
|
|
45
|
+
note('Tip: keep the terminal open while you want this device online.');
|
|
46
|
+
note('Only system information is shared unless you specify an allowed folder.');
|
|
47
|
+
}
|
|
48
|
+
export function ready(details) {
|
|
49
|
+
console.log('');
|
|
50
|
+
divider();
|
|
51
|
+
success('Secure connection established');
|
|
52
|
+
success('Device is online');
|
|
53
|
+
console.log('');
|
|
54
|
+
info('Device',details.name);
|
|
55
|
+
info('Account',details.account||'Your vo1d workspace');
|
|
56
|
+
info('Device ID',details.id);
|
|
57
|
+
info('Server',details.server);
|
|
58
|
+
info('Status','Online');
|
|
59
|
+
info('Allowed roots',details.roots?.length?details.roots.join(' ; '):'None (system info only)');
|
|
60
|
+
console.log('');
|
|
61
|
+
heading('Next');
|
|
62
|
+
note('Return to your MCP-compatible assistant when the connection is enabled.');
|
|
63
|
+
note('Keep this terminal open, or minimize it.');
|
|
64
|
+
note('Press Ctrl+C to disconnect (not to log out).');
|
|
65
|
+
console.log('');
|
|
66
|
+
heading('Manage this device (in another terminal)');
|
|
67
|
+
note('Status: npx -y @vo1d7bin/remote-mcp-agent@latest status');
|
|
68
|
+
note('Log out: npx -y @vo1d7bin/remote-mcp-agent@latest logout');
|
|
69
|
+
console.log('');
|
|
70
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { banner, heading, info, success, note } from './cli-ux.mjs';
|
|
6
|
+
|
|
7
|
+
const profile = process.env.VO1D_CONFIG_DIR
|
|
8
|
+
? path.resolve(process.env.VO1D_CONFIG_DIR)
|
|
9
|
+
: path.join(os.homedir(), '.vo1d-remote-mcp');
|
|
10
|
+
const file=path.join(profile,'device.json');
|
|
11
|
+
const publicServer=process.env.VO1D_SERVER || 'https://mcp.thelinuxserver.cloud';
|
|
12
|
+
|
|
13
|
+
async function readProfile(){
|
|
14
|
+
let cfg;
|
|
15
|
+
try {cfg=JSON.parse(await fs.readFile(file,'utf8'));}
|
|
16
|
+
catch(error) {
|
|
17
|
+
if(error.code==='ENOENT')return null;
|
|
18
|
+
throw new Error('Unable to read device credentials: '+error.message);
|
|
19
|
+
}
|
|
20
|
+
if(typeof cfg.id!=='string' || typeof cfg.token!=='string' || cfg.token.length<30 ||
|
|
21
|
+
typeof cfg.server!=='string')throw new Error('Saved device credentials appear corrupted.');
|
|
22
|
+
const server=new URL(cfg.server);
|
|
23
|
+
if(server.protocol!=='https:' &&
|
|
24
|
+
!(server.protocol==='http:' && ['localhost','127.0.0.1'].includes(server.hostname)))
|
|
25
|
+
throw new Error('Saved profile has an invalid server address.');
|
|
26
|
+
return cfg;
|
|
27
|
+
}
|
|
28
|
+
async function call(cfg,endpoint,method='GET') {
|
|
29
|
+
const url=new URL(endpoint,cfg.server).href;
|
|
30
|
+
const response=await fetch(url,{method,headers:{Authorization:'Bearer '+cfg.token},signal:AbortSignal.timeout(12000)});
|
|
31
|
+
if(response.status===401)return {invalid:true};
|
|
32
|
+
if(!response.ok)throw new Error('Server responded HTTP '+response.status);
|
|
33
|
+
return response.json();
|
|
34
|
+
}
|
|
35
|
+
export async function showStatus(){
|
|
36
|
+
banner();
|
|
37
|
+
heading('Device status');
|
|
38
|
+
const cfg=await readProfile();
|
|
39
|
+
if(!cfg){
|
|
40
|
+
note('No saved device authorization for this profile.');
|
|
41
|
+
note('Start with: npx -y @vo1d7bin/remote-mcp-agent@latest remote');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
info('Device ID',cfg.id);
|
|
45
|
+
info('Server',cfg.server);
|
|
46
|
+
try {
|
|
47
|
+
const remote=await call(cfg,'/api/agent/me');
|
|
48
|
+
if(remote.invalid){
|
|
49
|
+
info('Status','Authorization invalid or revoked');
|
|
50
|
+
note('Use logout --local to clear this profile, then remote to reauthorize.');
|
|
51
|
+
process.exitCode=1;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
info('Device',remote.name);
|
|
55
|
+
info('Account',remote.account || 'vo1d account');
|
|
56
|
+
info('Status',remote.online?'Online':'Offline');
|
|
57
|
+
if(!remote.online)note('Run remote to bring this device online.');
|
|
58
|
+
}catch(error){
|
|
59
|
+
info('Status','Unknown (server unavailable)');
|
|
60
|
+
note(error.message);
|
|
61
|
+
process.exitCode=1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export async function logOut(){
|
|
65
|
+
banner();
|
|
66
|
+
heading('Log out this device');
|
|
67
|
+
const cfg=await readProfile();
|
|
68
|
+
if(!cfg){note('Already logged out. No device credentials found.');return;}
|
|
69
|
+
if(process.argv.includes('--local')){
|
|
70
|
+
await fs.unlink(file);
|
|
71
|
+
note('Local credentials deleted, but server authorization was NOT revoked.');
|
|
72
|
+
note('Revoke this device in the online dashboard if it is still authorized.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const result=await call(cfg,'/api/agent/revoke-self','POST');
|
|
77
|
+
if(!result.revoked && !result.invalid)throw new Error('Server did not confirm revocation');
|
|
78
|
+
}catch(error){
|
|
79
|
+
console.error('Could not revoke device: '+error.message);
|
|
80
|
+
note('Credentials have been kept so you can retry safely.');
|
|
81
|
+
note('Alternatively, revoke from the dashboard. --local only deletes locally.');
|
|
82
|
+
process.exitCode=1;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await fs.unlink(file);
|
|
86
|
+
success('Server authorization revoked');
|
|
87
|
+
success('Local device credentials removed');
|
|
88
|
+
note('Run remote whenever you want to authorize this machine again.');
|
|
89
|
+
}
|
|
90
|
+
export async function openDashboard(){
|
|
91
|
+
banner();
|
|
92
|
+
const cfg=await readProfile();
|
|
93
|
+
const origin=cfg?.server || publicServer;
|
|
94
|
+
const url=new URL(origin);
|
|
95
|
+
if(url.protocol!=='https:' && !(url.protocol==='http:'&&['localhost','127.0.0.1'].includes(url.hostname)))
|
|
96
|
+
throw new Error('Dashboard URL must be HTTPS');
|
|
97
|
+
const address=url.origin+'/dashboard';
|
|
98
|
+
console.log('Dashboard:',address);
|
|
99
|
+
if(process.argv.includes('--no-browser') || process.env.VO1D_NO_BROWSER==='true')return;
|
|
100
|
+
const command=process.platform==='win32'?'explorer.exe':process.platform==='darwin'?'open':'xdg-open';
|
|
101
|
+
const child=spawn(command,[address],{stdio:'ignore',detached:true,windowsHide:true});
|
|
102
|
+
child.on('error',()=>note('Could not open browser; use the URL above.'));
|
|
103
|
+
child.unref();
|
|
104
|
+
}
|
package/bin/vo1d-agent.mjs
CHANGED
|
@@ -6,6 +6,8 @@ import path from 'node:path';
|
|
|
6
6
|
import readline from 'node:readline/promises';
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
8
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
9
|
+
import { banner, heading, info, success, note, divider, help, ready, version } from './cli-ux.mjs';
|
|
10
|
+
import { showStatus, logOut, openDashboard } from './management.mjs';
|
|
9
11
|
|
|
10
12
|
const folder = process.env.VO1D_CONFIG_DIR
|
|
11
13
|
? path.resolve(process.env.VO1D_CONFIG_DIR)
|
|
@@ -145,6 +147,8 @@ async function directoryListing(requestPath,roots) {
|
|
|
145
147
|
}))};
|
|
146
148
|
}
|
|
147
149
|
async function remote() {
|
|
150
|
+
banner();
|
|
151
|
+
heading('Remote connection');
|
|
148
152
|
const roots=await allowedRoots();
|
|
149
153
|
const cfg=await loadOrEnroll();
|
|
150
154
|
const server=normalizedServer(cfg.server);
|
|
@@ -155,7 +159,22 @@ async function remote() {
|
|
|
155
159
|
async function connect() {
|
|
156
160
|
if(stopped)return;
|
|
157
161
|
const ws=new WebSocket(endpoint,{headers:{Authorization:'Bearer '+cfg.token},handshakeTimeout:12000});
|
|
158
|
-
ws.on('open',()=>{
|
|
162
|
+
ws.on('open',async()=>{
|
|
163
|
+
retry=1000;
|
|
164
|
+
console.log('Connected to',server);
|
|
165
|
+
let details={id:cfg.id,name:os.hostname(),account:null,server,roots};
|
|
166
|
+
try {
|
|
167
|
+
const response=await fetch(server+'/api/agent/me',{
|
|
168
|
+
headers:{Authorization:'Bearer '+cfg.token},
|
|
169
|
+
signal:AbortSignal.timeout(9000)
|
|
170
|
+
});
|
|
171
|
+
if(response.ok){
|
|
172
|
+
const remote=await response.json();
|
|
173
|
+
details={...details,name:remote.name||details.name,account:remote.account};
|
|
174
|
+
}
|
|
175
|
+
}catch{note('Account details unavailable; relay connection is still active.');}
|
|
176
|
+
if(ws.readyState===WebSocket.OPEN)ready(details);
|
|
177
|
+
});
|
|
159
178
|
ws.on('message',async raw=>{
|
|
160
179
|
let task;
|
|
161
180
|
try{task=JSON.parse(raw.toString());}catch{return;}
|
|
@@ -188,8 +207,12 @@ async function remote() {
|
|
|
188
207
|
await connect();
|
|
189
208
|
}
|
|
190
209
|
const command=process.argv[2];
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
else
|
|
194
|
-
|
|
195
|
-
|
|
210
|
+
const run = fn => fn().catch(error=>{console.error('Error:',error.message);process.exitCode=1;});
|
|
211
|
+
if(command==='--version' || command==='-v' || command==='version') console.log('vo1d-agent v'+version);
|
|
212
|
+
else if(command==='--help' || command==='-h' || command==='help' || command==='remote' && process.argv.includes('--help')) help();
|
|
213
|
+
else if(command==='logout' || command==='remote' && process.argv.includes('--logout')) run(logOut);
|
|
214
|
+
else if(command==='status' || command==='whoami') run(showStatus);
|
|
215
|
+
else if(command==='open' || command==='dashboard') run(openDashboard);
|
|
216
|
+
else if(command==='enroll') run(legacyEnroll);
|
|
217
|
+
else if(!command || command==='remote') run(remote);
|
|
218
|
+
else {console.error('Unknown command:',command);help();process.exitCode=1;}
|