@remcp/remcp 0.1.0 → 0.1.1

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/src/agent.mjs CHANGED
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import WebSocket from 'ws';
7
7
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
8
8
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
9
+ import { VERSION } from './version.mjs';
9
10
 
10
11
  function desktopCommanderEntry() {
11
12
  try { return fileURLToPath(import.meta.resolve('@wonderwhy-er/desktop-commander')); } catch {}
@@ -31,10 +32,10 @@ export async function runAgent(options) {
31
32
  let stopping = false;
32
33
  let activeSocket;
33
34
 
34
- const mcp = new Client({ name: 'remcp-agent', version: '0.1.0' });
35
+ const mcp = new Client({ name: 'remcp-agent', version: VERSION });
35
36
  const transport = new StdioClientTransport({ command: process.execPath, args: [desktopCommanderEntry()] });
36
37
  await mcp.connect(transport);
37
- console.log('Local Desktop Commander MCP ready');
38
+ console.log('Local device runtime ready');
38
39
 
39
40
  async function respond(ws, message) {
40
41
  try {
@@ -54,7 +55,7 @@ export async function runAgent(options) {
54
55
  const ws = new WebSocket(agentUrl, { headers: { Authorization: `Bearer ${deviceToken}` } });
55
56
  activeSocket = ws;
56
57
  ws.on('open', () => {
57
- ws.send(JSON.stringify({ type: 'hello', deviceId, deviceName, hostname: os.hostname(), platform: process.platform, arch: process.arch, agentVersion: '0.1.0' }));
58
+ ws.send(JSON.stringify({ type: 'hello', deviceId, deviceName, hostname: os.hostname(), platform: process.platform, arch: process.arch, agentVersion: VERSION }));
58
59
  console.log(`Connected to ${agentUrl} as ${deviceName} (${deviceId})`);
59
60
  });
60
61
  ws.on('message', raw => {
package/src/cli.mjs CHANGED
@@ -2,11 +2,15 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import process from 'node:process';
5
+ import { spawnSync } from 'node:child_process';
5
6
  import { runAgent } from './agent.mjs';
7
+ import { LOCAL_RUNTIME_NAME, LOCAL_RUNTIME_SPEC, PACKAGE_NAME, VERSION } from './version.mjs';
6
8
 
7
9
  const home = os.homedir();
8
10
  const configDir = process.env.REMCP_CONFIG_DIR || path.join(home, '.config', 'remcp');
9
11
  const configFile = path.join(configDir, 'config.json');
12
+ const serviceFile = path.join(home, '.config', 'systemd', 'user', 'remcp-agent.service');
13
+ const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
10
14
 
11
15
  function parse(argv) {
12
16
  const [command = 'help', ...rest] = argv;
@@ -18,73 +22,141 @@ function parse(argv) {
18
22
  }
19
23
  return { command, flags };
20
24
  }
25
+
26
+ function run(command, args, options = {}) {
27
+ const result = spawnSync(command, args, { stdio: 'inherit', ...options });
28
+ if (result.error) throw result.error;
29
+ if (result.status !== 0) throw new Error(`${command} failed with exit code ${result.status}`);
30
+ return result;
31
+ }
32
+
33
+ function output(command, args) {
34
+ const result = spawnSync(command, args, { encoding: 'utf8' });
35
+ if (result.error) throw result.error;
36
+ if (result.status !== 0) throw new Error(`${command} failed with exit code ${result.status}`);
37
+ return String(result.stdout || '').trim();
38
+ }
39
+
21
40
  function loadConfig() {
22
- if (!fs.existsSync(configFile)) throw new Error(`ReMCP is not paired. Run: remcp connect --server https://host --code CODE`);
41
+ if (!fs.existsSync(configFile)) throw new Error(`ReMCP is not paired. Run: npx --yes ${PACKAGE_NAME}@latest connect --server https://remcp.delio24.com --code CODE --install`);
23
42
  return JSON.parse(fs.readFileSync(configFile, 'utf8'));
24
43
  }
44
+
25
45
  function saveConfig(value) {
26
46
  fs.mkdirSync(configDir, { recursive: true, mode: 0o700 });
27
47
  fs.writeFileSync(configFile, JSON.stringify(value, null, 2) + '\n', { mode: 0o600 });
28
48
  fs.chmodSync(configFile, 0o600);
29
49
  }
30
- function unitPath() { return path.join(home, '.config', 'systemd', 'user', 'remcp-agent.service'); }
31
- function installLinuxService() {
32
- const unit = `[Unit]\nDescription=ReMCP device agent\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nExecStart=${process.execPath} ${process.argv[1]} start\nRestart=always\nRestartSec=3\nNoNewPrivileges=true\n\n[Install]\nWantedBy=default.target\n`;
33
- fs.mkdirSync(path.dirname(unitPath()), { recursive: true });
34
- fs.writeFileSync(unitPath(), unit);
35
- const { spawnSync } = requireSpawn();
50
+
51
+ function globalPrefix() {
52
+ return output(npmCommand, ['prefix', '--global']);
53
+ }
54
+
55
+ function globalCliPath() {
56
+ const prefix = globalPrefix();
57
+ return process.platform === 'win32' ? path.join(prefix, 'remcp.cmd') : path.join(prefix, 'bin', 'remcp');
58
+ }
59
+
60
+ function npmGlobalInstall(packageSpec) {
61
+ run(npmCommand, ['install', '--global', packageSpec, LOCAL_RUNTIME_SPEC, '--no-audit', '--no-fund', '--loglevel=error']);
62
+ }
63
+
64
+ function quoteSystemd(value) {
65
+ return `"${String(value).replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
66
+ }
67
+
68
+ function installLinuxService(cliPath = globalCliPath()) {
69
+ const unit = `[Unit]\nDescription=ReMCP device agent\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=simple\nExecStart=${quoteSystemd(cliPath)} start\nRestart=always\nRestartSec=3\nNoNewPrivileges=true\n\n[Install]\nWantedBy=default.target\n`;
70
+ fs.mkdirSync(path.dirname(serviceFile), { recursive: true });
71
+ fs.writeFileSync(serviceFile, unit);
72
+ run('systemctl', ['--user', 'daemon-reload']);
73
+ run('systemctl', ['--user', 'enable', '--now', 'remcp-agent.service']);
74
+ }
75
+
76
+ function installPersistentAgent() {
77
+ if (process.platform !== 'linux') throw new Error('Automatic background service installation currently supports Linux');
78
+ console.log(`Installing ${PACKAGE_NAME}@${VERSION} and the local device runtime…`);
79
+ npmGlobalInstall(`${PACKAGE_NAME}@${VERSION}`);
80
+ installLinuxService(globalCliPath());
81
+ console.log('ReMCP is installed as a user service. Future updates: remcp update');
82
+ }
83
+
84
+ function restartLinuxServiceIfInstalled() {
85
+ if (process.platform !== 'linux' || !fs.existsSync(serviceFile)) return;
86
+ run('systemctl', ['--user', 'daemon-reload']);
87
+ run('systemctl', ['--user', 'restart', 'remcp-agent.service']);
88
+ }
89
+
90
+ function uninstallLinuxService() {
91
+ if (process.platform !== 'linux') return;
92
+ spawnSync('systemctl', ['--user', 'disable', '--now', 'remcp-agent.service'], { stdio: 'inherit' });
93
+ try { fs.unlinkSync(serviceFile); } catch {}
36
94
  spawnSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'inherit' });
37
- spawnSync('systemctl', ['--user', 'enable', '--now', 'remcp-agent.service'], { stdio: 'inherit' });
38
95
  }
39
- function requireSpawn() {
40
- return { spawnSync: (command, args, opts) => {
41
- const result = requireChild().spawnSync(command, args, opts);
42
- if (result.status !== 0) throw new Error(`${command} failed`);
43
- return result;
44
- }};
96
+
97
+ function printHelp() {
98
+ console.log(`ReMCP ${VERSION}\n\nCommands:\n npx --yes ${PACKAGE_NAME}@latest connect --server https://remcp.delio24.com --code ABC12345 --install\n remcp start\n remcp status\n remcp doctor\n remcp update\n remcp install\n remcp uninstall\n remcp uninstall --purge\n remcp --version`);
45
99
  }
46
- let childModule;
47
- function requireChild() { if (!childModule) throw new Error('child_process not initialized'); return childModule; }
48
100
 
49
101
  export async function main(argv = process.argv.slice(2)) {
50
- childModule = await import('node:child_process');
51
102
  const { command, flags } = parse(argv);
103
+
104
+ if (command === '--version' || command === '-v' || command === 'version') {
105
+ console.log(VERSION);
106
+ return;
107
+ }
108
+
52
109
  if (command === 'connect') {
53
110
  const server = String(flags.server || '').replace(/\/$/, '');
54
111
  const code = String(flags.code || '').toUpperCase();
55
112
  if (!server || !code) throw new Error('--server and --code are required');
56
113
  const response = await fetch(`${server}/api/pair/claim`, {
57
- method: 'POST', headers: { 'content-type': 'application/json' },
114
+ method: 'POST',
115
+ headers: { 'content-type': 'application/json' },
58
116
  body: JSON.stringify({ code, name: String(flags.name || os.hostname()), hostname: os.hostname(), platform: process.platform, arch: process.arch }),
59
117
  });
60
118
  if (!response.ok) throw new Error(`Pairing failed (${response.status}): ${await response.text()}`);
61
119
  const paired = await response.json();
62
120
  saveConfig({ serverUrl: server, deviceId: paired.deviceId, deviceToken: paired.deviceToken, deviceName: String(flags.name || os.hostname()) });
63
121
  console.log(`Paired ${os.hostname()} with ${server}`);
64
- if (flags.install) {
65
- if (process.platform === 'linux') installLinuxService();
66
- else console.log('Automatic service installation currently supports Linux. Run `remcp start` or install a launch service for this user.');
67
- }
122
+ if (flags.install) installPersistentAgent();
68
123
  return;
69
124
  }
70
- if (command === 'start') { await runAgent(loadConfig()); return; }
125
+
126
+ if (command === 'start') {
127
+ await runAgent(loadConfig());
128
+ return;
129
+ }
130
+
71
131
  if (command === 'status' || command === 'doctor') {
72
132
  const cfg = loadConfig();
73
- const health = await fetch(`${cfg.serverUrl}/health`).then(r => r.json());
74
- console.log(JSON.stringify({ configured: true, deviceId: cfg.deviceId, deviceName: cfg.deviceName, server: cfg.serverUrl, serverHealth: health }, null, 2));
133
+ const health = await fetch(`${cfg.serverUrl}/health?fresh=${Date.now()}`, { cache: 'no-store' }).then(r => r.json());
134
+ console.log(JSON.stringify({ configured: true, cliVersion: VERSION, deviceId: cfg.deviceId, deviceName: cfg.deviceName, server: cfg.serverUrl, serverHealth: health }, null, 2));
75
135
  return;
76
136
  }
137
+
77
138
  if (command === 'install') {
78
- if (process.platform !== 'linux') throw new Error('Automatic service installation currently supports Linux');
79
- loadConfig(); installLinuxService(); return;
139
+ loadConfig();
140
+ installPersistentAgent();
141
+ return;
80
142
  }
143
+
144
+ if (command === 'update') {
145
+ console.log(`Updating ${PACKAGE_NAME} to the latest published version…`);
146
+ npmGlobalInstall(`${PACKAGE_NAME}@latest`);
147
+ restartLinuxServiceIfInstalled();
148
+ console.log('ReMCP updated. Run `remcp --version` or `remcp status` to verify.');
149
+ return;
150
+ }
151
+
81
152
  if (command === 'uninstall') {
82
- if (process.platform === 'linux') {
83
- childModule.spawnSync('systemctl', ['--user','disable','--now','remcp-agent.service'], { stdio: 'inherit' });
84
- try { fs.unlinkSync(unitPath()); } catch {}
85
- childModule.spawnSync('systemctl', ['--user','daemon-reload'], { stdio: 'inherit' });
153
+ uninstallLinuxService();
154
+ if (flags.purge) {
155
+ console.log('Removing global ReMCP packages…');
156
+ run(npmCommand, ['uninstall', '--global', PACKAGE_NAME, LOCAL_RUNTIME_NAME, '--no-audit', '--no-fund', '--loglevel=error']);
86
157
  }
87
158
  return;
88
159
  }
89
- console.log(`ReMCP 0.1.0\n\nCommands:\n remcp connect --server https://remcp.delio24.com --code ABC12345 --install\n remcp start\n remcp status\n remcp install\n remcp uninstall`);
160
+
161
+ printHelp();
90
162
  }
package/src/config.mjs CHANGED
@@ -22,7 +22,7 @@ function loadSigningSecret() {
22
22
  return generated;
23
23
  }
24
24
  const tokenSecret = loadSigningSecret();
25
- const installSpec = (process.env.REMCP_INSTALL_SPEC || '@remcp/remcp').trim();
25
+ const installSpec = (process.env.REMCP_INSTALL_SPEC || '@remcp/remcp@latest').trim();
26
26
  if (!/^[A-Za-z0-9@._:/#-]+$/.test(installSpec)) throw new Error('REMCP_INSTALL_SPEC contains unsupported characters');
27
27
 
28
28
  export const config = Object.freeze({
package/src/mcp.mjs CHANGED
@@ -1,13 +1,14 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
+ import { VERSION } from './version.mjs';
4
5
 
5
6
  const rawCatalog = JSON.parse(readFileSync(new URL('./tool-catalog.json', import.meta.url), 'utf8'));
6
7
  const destructive = new Set(['set_config_value','write_file','write_pdf','move_file','edit_block','start_process','interact_with_process','force_terminate','kill_process']);
7
8
  const openWorld = new Set(['start_process','interact_with_process']);
8
9
  const descriptions = {
9
- get_config: 'Read the local Desktop Commander configuration for the selected paired device.',
10
- set_config_value: 'Change one supported Desktop Commander configuration value on the selected paired device.',
10
+ get_config: 'Read the local device runtime configuration for the selected paired device.',
11
+ set_config_value: 'Change one supported local device runtime configuration value on the selected paired device.',
11
12
  read_file: 'Read a local file or a user-supplied URL from the selected paired device.',
12
13
  read_multiple_files: 'Read multiple local files from the selected paired device in one call.',
13
14
  write_file: 'Create, replace, or append to a file on the selected paired device.',
@@ -28,10 +29,14 @@ const descriptions = {
28
29
  list_sessions: 'List active terminal sessions on the selected paired device.',
29
30
  list_processes: 'List running operating-system processes on the selected paired device.',
30
31
  kill_process: 'Terminate a process by PID on the selected paired device.',
31
- get_usage_stats: 'Read local Desktop Commander usage statistics from the selected paired device.',
32
- get_recent_tool_calls: 'Read recent local Desktop Commander tool-call history from the selected paired device.',
32
+ get_usage_stats: 'Read local device runtime usage statistics from the selected paired device.',
33
+ get_recent_tool_calls: 'Read recent local device runtime tool-call history from the selected paired device.',
33
34
  };
34
35
 
36
+ function publicDescription(tool) {
37
+ return String(descriptions[tool.name] || tool.description || '').replace(/\s+/g, ' ').trim();
38
+ }
39
+
35
40
  function publicTool(tool) {
36
41
  const schema = structuredClone(tool.inputSchema || { type: 'object', properties: {} });
37
42
  schema.type = 'object';
@@ -45,7 +50,7 @@ function publicTool(tool) {
45
50
  return {
46
51
  name: tool.name,
47
52
  title: upstream.title || tool.name,
48
- description: descriptions[tool.name] || tool.description,
53
+ description: publicDescription(tool),
49
54
  inputSchema: schema,
50
55
  annotations: {
51
56
  title: upstream.title || tool.name,
@@ -68,7 +73,7 @@ const listDevicesTool = {
68
73
  };
69
74
 
70
75
  export function createMcpServer(uid, relay) {
71
- const server = new Server({ name: 'remcp', version: '0.1.0' }, { capabilities: { tools: {} } });
76
+ const server = new Server({ name: 'remcp', version: VERSION }, { capabilities: { tools: {} } });
72
77
 
73
78
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [listDevicesTool, ...catalog] }));
74
79
  server.setRequestHandler(CallToolRequestSchema, async request => {
package/src/server.mjs CHANGED
@@ -13,6 +13,7 @@ import { oauthRouter } from './oauth.mjs';
13
13
  import { createRelay } from './relay.mjs';
14
14
  import { createMcpServer } from './mcp.mjs';
15
15
  import { now, randomId, randomToken, sha256 } from './util.mjs';
16
+ import { VERSION } from './version.mjs';
16
17
 
17
18
  const app = express();
18
19
  app.set('trust proxy', 1);
@@ -22,15 +23,15 @@ app.use(helmet({
22
23
  contentSecurityPolicy: {
23
24
  directives: {
24
25
  defaultSrc: ["'self'"],
25
- scriptSrc: ["'self'", 'https://www.gstatic.com'],
26
- connectSrc: ["'self'", 'https://identitytoolkit.googleapis.com', 'https://securetoken.googleapis.com', 'https://www.googleapis.com'],
26
+ scriptSrc: ["'self'", 'https://www.gstatic.com', 'https://apis.google.com', 'https://www.google.com'],
27
+ connectSrc: ["'self'", 'https://identitytoolkit.googleapis.com', 'https://securetoken.googleapis.com', 'https://www.googleapis.com', 'https://apis.google.com', 'https://accounts.google.com'],
27
28
  imgSrc: ["'self'", 'data:'],
28
29
  styleSrc: ["'self'"],
29
30
  fontSrc: ["'none'"],
30
- frameSrc: [firebaseFrame, 'https://accounts.google.com'],
31
+ frameSrc: [firebaseFrame, 'https://accounts.google.com', 'https://apis.google.com'],
31
32
  objectSrc: ["'none'"],
32
33
  baseUri: ["'none'"],
33
- formAction: ["'self'"],
34
+ formAction: ["'self'", 'https://accounts.google.com'],
34
35
  frameAncestors: ["'none'"],
35
36
  },
36
37
  },
@@ -57,7 +58,7 @@ const relay = createRelay(httpServer);
57
58
  const sessions = new Map();
58
59
  const publicDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../public');
59
60
 
60
- app.get('/health', (_req, res) => res.json({ ok: true, service: 'remcp', version: '0.1.0', connectedDevices: relay.onlineCount() }));
61
+ app.get('/health', (_req, res) => res.json({ ok: true, service: 'remcp', version: VERSION, connectedDevices: relay.onlineCount() }));
61
62
  app.get(config.domainVerificationPath, (_req, res) => {
62
63
  if (!config.domainVerificationToken) return res.status(404).type('text/plain').send('Not configured');
63
64
  res.type('text/plain').send(config.domainVerificationToken);