@remcp/remcp 0.1.0 → 0.1.2

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/public/style.css CHANGED
@@ -87,6 +87,19 @@ main { display: block; }
87
87
  .principles h2 { margin: 28px 0 9px; font-size: 21px; line-height: 1.2; letter-spacing: -.025em; }
88
88
  .principles p { max-width: 330px; margin: 0; color: var(--muted); font-size: 15px; }
89
89
 
90
+
91
+ .quickstart { max-width: var(--container); margin: 0 auto; padding: clamp(72px, 9vw, 104px) 20px; border-top: 1px solid var(--line); }
92
+ .quickstart .section-heading { margin-left: 0; margin-right: 0; }
93
+ .quickstart-grid { display: grid; grid-template-columns: 1.35fr .8fr .8fr; gap: 0; margin: 34px 0 26px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
94
+ .command-card { min-width: 0; padding: 28px 28px 30px 0; }
95
+ .command-card + .command-card { padding-left: 28px; border-left: 1px solid var(--line); }
96
+ .command-card > span { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
97
+ .command-card h3 { margin: 18px 0 8px; font-size: 20px; letter-spacing: -.025em; }
98
+ .command-card p { margin: 12px 0 0; color: var(--muted); font-size: 14px; }
99
+ .command-card .command-row { margin-top: 14px; }
100
+ .site-header nav { min-width: 0; overflow-x: auto; scrollbar-width: none; }
101
+ .site-header nav::-webkit-scrollbar { display: none; }
102
+
90
103
  .workspace { padding: clamp(72px, 9vw, 112px) 20px; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); background: var(--surface); }
91
104
  .workspace > * { max-width: var(--container); margin-left: auto; margin-right: auto; }
92
105
  .section-heading { max-width: 720px; margin-bottom: 45px; }
@@ -107,7 +120,8 @@ input:focus-visible { border-color: var(--accent); }
107
120
  .button-row { display: flex; gap: 9px; margin-top: 5px; }
108
121
  .divider { display: flex; align-items: center; gap: 11px; margin: 7px 0; color: var(--muted); font-size: 12px; }
109
122
  .divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--line); }
110
- .error { min-height: 20px; margin: 2px 0 0; color: var(--danger); font-size: 13px; }
123
+ .error { margin: 2px 0 0; color: var(--danger); font-size: 13px; }
124
+ p:empty, .error:empty, #status:empty { display: none; }
111
125
  .identity { display: flex; align-items: center; gap: 11px; }
112
126
  .identity small { display: block; color: var(--muted); }
113
127
  .status-dot { width: 9px; height: 9px; flex: 0 0 9px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 0 4px var(--ok-soft); }
@@ -141,24 +155,27 @@ input:focus-visible { border-color: var(--accent); }
141
155
  .site-footer { max-width: var(--container); min-height: 86px; margin: 0 auto; padding: 22px 20px; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 26px; border-top: 1px solid var(--line); font-size: 13px; }
142
156
  .site-footer p { margin: 0; color: var(--muted); text-align: right; }
143
157
 
144
- .doc-shell { width: min(100%, var(--container)); margin: 0 auto; padding: 64px 20px 112px; display: grid; grid-template-columns: 190px minmax(0, 760px); gap: clamp(48px, 8vw, 96px); }
158
+ .doc-shell { width: min(100%, var(--container)); margin: 0 auto; padding: 48px 20px 76px; display: grid; grid-template-columns: 176px minmax(0, 760px); gap: clamp(36px, 6vw, 72px); }
145
159
  .doc-nav { position: sticky; top: 28px; align-self: start; display: flex; flex-direction: column; gap: 4px; }
146
160
  .doc-nav a { padding: 8px 10px; border-radius: 7px; color: var(--muted); font-size: 14px; font-weight: 550; }
147
161
  .doc-nav a:hover { background: var(--surface-subtle); color: var(--ink); }
148
162
  .doc-nav a[aria-current="page"] { background: var(--surface-subtle); color: var(--ink); font-weight: 680; }
149
163
  .prose { min-width: 0; }
150
- .prose h1 { margin: 0 0 22px; max-width: 740px; font-size: clamp(42px, 5.5vw, 62px); line-height: 1.02; letter-spacing: -.05em; }
151
- .prose h2 { scroll-margin-top: 28px; margin: 52px 0 12px; font-size: 25px; line-height: 1.2; letter-spacing: -.035em; }
152
- .prose h3 { scroll-margin-top: 28px; margin: 31px 0 8px; font-size: 18px; }
153
- .prose p, .prose li { color: #505057; font-size: 16px; text-wrap: pretty; }
154
- .prose p { margin: 0 0 16px; }
164
+ .prose h1 { margin: 0 0 18px; max-width: 700px; font-size: clamp(38px, 5vw, 54px); line-height: 1.04; letter-spacing: -.045em; }
165
+ .prose h2 { scroll-margin-top: 24px; margin: 36px 0 9px; font-size: 23px; line-height: 1.22; letter-spacing: -.03em; }
166
+ .prose h3 { scroll-margin-top: 24px; margin: 24px 0 7px; font-size: 17px; }
167
+ .prose p, .prose li { color: #505057; font-size: 15.5px; text-wrap: pretty; }
168
+ .prose p { margin: 0 0 12px; }
155
169
  .prose a { color: #3538ae; text-decoration: underline; text-decoration-color: #a5a7e9; text-underline-offset: 3px; }
156
170
  .prose a:hover { text-decoration-color: #3538ae; }
157
- .prose pre { overflow: auto; margin: 18px 0 24px; padding: 16px; border-radius: 10px; background: #1e1e21; color: #f4f4f2; }
171
+ .prose pre { min-width: 0; overflow: auto; margin: 0; padding: 13px 14px; background: transparent; color: #f4f4f2; }
172
+ .prose .code-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px; margin: 12px 0 16px; overflow: hidden; border-radius: 10px; background: #1e1e21; }
173
+ .prose .code-row .code-copy { min-height: 34px; margin-right: 8px; padding: 7px 10px; border-color: #4b4b50; background: #2b2b2f; color: #f4f4f2; font-size: 12px; white-space: nowrap; }
174
+ .prose .code-row .code-copy:hover { border-color: #66666d; background: #38383d; }
158
175
  .prose code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .9em; }
159
176
  .prose :not(pre) > code { padding: 2px 5px; border-radius: 5px; background: var(--surface-subtle); color: #34343a; }
160
177
  .prose hr { margin: 46px 0; border: 0; border-top: 1px solid var(--line); }
161
- .prose .notice { margin: 28px 0; padding: 16px 18px; border: 1px solid #d8d8f7; border-radius: 9px; background: var(--accent-soft); }
178
+ .prose .notice { margin: 20px 0; padding: 13px 15px; border: 1px solid #d8d8f7; border-radius: 9px; background: var(--accent-soft); }
162
179
  .prose .notice p { margin: 5px 0 0; }
163
180
 
164
181
  .oauth-shell { min-height: calc(100svh - 68px); display: grid; place-items: center; padding: 56px 20px 80px; }
@@ -184,10 +201,13 @@ input:focus-visible, button:focus-visible, a:focus-visible { outline: 3px solid
184
201
  .principles > div, .principles > div + div { min-height: auto; padding: 28px 0; border-left: 0; border-bottom: 1px solid var(--line); }
185
202
  .principles > div:last-child { border-bottom: 0; }
186
203
  .workspace-grid { grid-template-columns: 1fr; }
204
+ .quickstart-grid { grid-template-columns: 1fr; }
205
+ .command-card, .command-card + .command-card { padding: 24px 0; border-left: 0; border-bottom: 1px solid var(--line); }
206
+ .command-card:last-child { border-bottom: 0; }
187
207
  .auth-pane { padding-right: 0; border-right: 0; border-bottom: 1px solid var(--line); }
188
208
  .endpoint-pane { padding-left: 0; }
189
- .doc-shell { grid-template-columns: 1fr; gap: 38px; padding-top: 42px; }
190
- .doc-nav { position: static; flex-direction: row; flex-wrap: wrap; gap: 4px; padding-bottom: 18px; border-bottom: 1px solid var(--line); }
209
+ .doc-shell { grid-template-columns: 1fr; gap: 0; padding-top: 34px; }
210
+ .doc-nav { display: none; }
191
211
  .site-footer { grid-template-columns: 1fr; }
192
212
  .site-footer nav { flex-wrap: wrap; }
193
213
  .site-footer p { text-align: left; }
@@ -196,7 +216,7 @@ input:focus-visible, button:focus-visible, a:focus-visible { outline: 3px solid
196
216
  @media (max-width: 620px) {
197
217
  .site-header { min-height: 62px; padding: 0 16px; }
198
218
  .site-header { gap: 12px; }
199
- .site-header nav { gap: 10px; }
219
+ .site-header nav { max-width: calc(100vw - 116px); gap: 12px; }
200
220
  .site-header nav a { font-size: 12.5px; }
201
221
  .hero { padding: 58px 18px 64px; }
202
222
  .hero h1 { font-size: clamp(46px, 14vw, 60px); }
@@ -205,21 +225,29 @@ input:focus-visible, button:focus-visible, a:focus-visible { outline: 3px solid
205
225
  .node { align-items: flex-start; flex-direction: column; gap: 3px; }
206
226
  .node.device-node { padding-right: 18px; }
207
227
  .node .pulse { top: 15px; }
208
- .workspace, .final-cta { padding-left: 18px; padding-right: 18px; }
228
+ .workspace, .final-cta, .quickstart { padding: 50px 18px; }
229
+ .section-heading { margin-bottom: 30px; }
230
+ .section-heading h2, .final-cta h2 { font-size: 36px; }
231
+ .section-heading > p:last-child, .final-cta > p { margin-top: 12px; font-size: 16px; }
209
232
  .button-row, .oauth-actions { flex-direction: column; }
210
233
  .button-row .button, .oauth-actions .button { width: 100%; }
211
234
  .pair-panel { grid-template-columns: 1fr; }
212
235
  .pair-panel .button { justify-self: start; }
213
236
  .panel-title { align-items: flex-start; flex-direction: column; }
214
- .command-row, .pairing-output { grid-template-columns: 1fr; }
215
- .copy-button { justify-self: start; }
237
+ .command-row, .pairing-output { grid-template-columns: minmax(0, 1fr) auto; }
238
+ .copy-button { justify-self: stretch; }
216
239
  .endpoint-facts div { align-items: flex-start; flex-direction: column; gap: 2px; }
217
240
  .endpoint-facts dd { text-align: left; }
218
241
  .site-footer { padding: 24px 18px; }
219
242
  .site-footer nav { gap: 14px; }
220
- .doc-shell { padding: 34px 18px 80px; }
221
- .doc-nav { overflow-x: auto; flex-wrap: nowrap; margin: 0 -18px; padding: 0 18px 16px; }
222
- .doc-nav a { white-space: nowrap; }
243
+ .doc-shell { gap: 18px; padding: 24px 16px 56px; }
244
+ .prose h1 { margin-bottom: 14px; font-size: clamp(34px, 10vw, 42px); }
245
+ .prose h2 { margin-top: 28px; font-size: 21px; }
246
+ .prose h3 { margin-top: 21px; }
247
+ .prose p, .prose li { font-size: 15px; }
248
+ .prose .code-row { margin: 10px 0 14px; }
249
+ .prose .code-row pre { padding: 12px 10px 12px 13px; }
250
+ .prose .code-row .code-copy { margin-right: 7px; }
223
251
  .oauth-shell { min-height: calc(100svh - 62px); padding: 28px 14px 60px; place-items: start center; }
224
252
  .oauth-card { padding: 22px 18px; }
225
253
  .scope-list li { align-items: flex-start; flex-direction: column; gap: 4px; }
@@ -8,7 +8,7 @@
8
8
  <title>Support · ReMCP</title>
9
9
  <meta name="description" content="Troubleshoot ReMCP sign-in, device pairing, agent connectivity and MCP authorization.">
10
10
  <link rel="preconnect" href="https://www.gstatic.com" crossorigin>
11
- <link rel="stylesheet" href="/style.css?v=b92ac4c9219a">
11
+ <link rel="stylesheet" href="/style.css?v=remcp-012">
12
12
  <meta property="og:site_name" content="ReMCP">
13
13
  <meta property="og:image" content="https://remcp.delio24.com/assets/og-remcp.png">
14
14
  <link rel="icon" href="/favicon.ico" sizes="any">
@@ -28,10 +28,10 @@
28
28
  <img class="brand-mark" src="/android-chrome-192x192.png" width="30" height="30" alt="" aria-hidden="true">
29
29
  <span translate="no">ReMCP</span>
30
30
  </a>
31
- <nav aria-label="Primary"><a href="/docs">Docs</a><a href="/security">Security</a><a href="/support" aria-current="page">Support</a></nav>
31
+ <nav aria-label="Primary"><a href="/docs">Docs</a><a href="/docs#install">Install</a><a href="/docs#update">Update</a><a href="/security">Security</a><a href="/support" aria-current="page">Support</a></nav>
32
32
  </header>
33
33
  <main id="main" class="doc-shell">
34
- <aside class="doc-nav" aria-label="Documentation"><a href="/docs">Setup</a><a href="/security">Security</a><a href="/support" aria-current="page">Support</a><a href="/privacy">Privacy</a><a href="/terms">Terms</a></aside>
34
+ <aside class="doc-nav" aria-label="Documentation"><a href="/docs">Setup</a><a href="/docs#install">Install</a><a href="/docs#update">Update</a><a href="/security">Security</a><a href="/support" aria-current="page">Support</a><a href="/privacy">Privacy</a><a href="/terms">Terms</a></aside>
35
35
  <article class="prose">
36
36
  <p class="eyebrow">Support</p>
37
37
  <h1>Troubleshoot ReMCP.</h1>
@@ -40,6 +40,9 @@
40
40
  <h2>OAuth does not complete</h2><p>Return to the authorization screen, sign in to the intended ReMCP account, review the requested access, and choose <strong>Authorize ReMCP Access</strong>.</p>
41
41
  <h2>MCP tool cannot run</h2><p>Confirm the target computer is online and use <code>list_devices</code> to obtain its current device id. Device tools require that id.</p>
42
42
  <h2>Pairing code expired</h2><p>Generate a new pairing command from the workspace. Pairing codes are intentionally short-lived and single-use.</p>
43
+ <h2>Check the installed agent</h2><div class="code-row"><pre><code id="support-command-1">remcp --version
44
+ remcp status</code></pre><button class="copy-button code-copy" type="button" data-copy="#support-command-1" aria-label="Copy command" aria-live="polite">Copy</button></div><p>If <code>remcp</code> is not found, repeat the <a href="/docs#install">install command</a>. The first npx invocation is temporary; <code>--install</code> creates the persistent CLI.</p>
45
+ <h2>Update or repair</h2><div class="code-row"><pre><code id="support-command-2">remcp update</code></pre><button class="copy-button code-copy" type="button" data-copy="#support-command-2" aria-label="Copy command" aria-live="polite">Copy</button></div><p>This refreshes the published CLI and restarts the installed user service. For an already paired device whose service is missing, run <code>remcp install</code>.</p>
43
46
  <h2>Need deployment help?</h2><p>For a self-hosted deployment, contact the operator who provisioned your ReMCP instance and include the affected layer: sign-in, pairing, agent connectivity, or MCP authorization.</p>
44
47
  </article>
45
48
  </main>
@@ -48,10 +51,9 @@
48
51
  <img class="brand-mark" src="/android-chrome-192x192.png" width="30" height="30" alt="" aria-hidden="true">
49
52
  <span translate="no">ReMCP</span>
50
53
  </div>
51
- <nav aria-label="Footer">
52
- <a href="/docs">Docs</a><a href="/security">Security</a><a href="/support">Support</a><a href="/privacy">Privacy</a><a href="/terms">Terms</a>
53
- </nav>
54
+ <nav aria-label="Footer"><a href="/docs">Docs</a><a href="/docs#install">Install</a><a href="/docs#update">Update</a><a href="/security">Security</a><a href="/support">Support</a><a href="/privacy">Privacy</a><a href="/terms">Terms</a></nav>
54
55
  <p>Open-source remote MCP infrastructure.</p>
55
56
  </footer>
57
+ <script type="module" src="/copy.js?v=remcp-012"></script>
56
58
  </body>
57
59
  </html>
package/public/terms.html CHANGED
@@ -8,7 +8,7 @@
8
8
  <title>Terms of Service · ReMCP</title>
9
9
  <meta name="description" content="Terms for using the hosted ReMCP service and its remote-computer capabilities.">
10
10
  <link rel="preconnect" href="https://www.gstatic.com" crossorigin>
11
- <link rel="stylesheet" href="/style.css?v=b92ac4c9219a">
11
+ <link rel="stylesheet" href="/style.css?v=remcp-012">
12
12
  <meta property="og:site_name" content="ReMCP">
13
13
  <meta property="og:image" content="https://remcp.delio24.com/assets/og-remcp.png">
14
14
  <link rel="icon" href="/favicon.ico" sizes="any">
@@ -28,10 +28,10 @@
28
28
  <img class="brand-mark" src="/android-chrome-192x192.png" width="30" height="30" alt="" aria-hidden="true">
29
29
  <span translate="no">ReMCP</span>
30
30
  </a>
31
- <nav aria-label="Primary"><a href="/docs">Docs</a><a href="/security">Security</a><a href="/support">Support</a></nav>
31
+ <nav aria-label="Primary"><a href="/docs">Docs</a><a href="/docs#install">Install</a><a href="/docs#update">Update</a><a href="/security">Security</a><a href="/support">Support</a></nav>
32
32
  </header>
33
33
  <main id="main" class="doc-shell">
34
- <aside class="doc-nav" aria-label="Legal and policy navigation"><a href="/docs">Setup</a><a href="/security">Security</a><a href="/support">Support</a><a href="/privacy">Privacy</a><a href="/terms" aria-current="page">Terms</a></aside>
34
+ <aside class="doc-nav" aria-label="Documentation"><a href="/docs">Setup</a><a href="/docs#install">Install</a><a href="/docs#update">Update</a><a href="/security">Security</a><a href="/support">Support</a><a href="/privacy">Privacy</a><a href="/terms" aria-current="page">Terms</a></aside>
35
35
  <article class="prose">
36
36
  <p class="eyebrow">Legal</p>
37
37
  <h1>Terms of Service</h1>
@@ -53,9 +53,7 @@
53
53
  <img class="brand-mark" src="/android-chrome-192x192.png" width="30" height="30" alt="" aria-hidden="true">
54
54
  <span translate="no">ReMCP</span>
55
55
  </div>
56
- <nav aria-label="Footer">
57
- <a href="/docs">Docs</a><a href="/security">Security</a><a href="/support">Support</a><a href="/privacy">Privacy</a><a href="/terms">Terms</a>
58
- </nav>
56
+ <nav aria-label="Footer"><a href="/docs">Docs</a><a href="/docs#install">Install</a><a href="/docs#update">Update</a><a href="/security">Security</a><a href="/support">Support</a><a href="/privacy">Privacy</a><a href="/terms">Terms</a></nav>
59
57
  <p>Open-source remote MCP infrastructure.</p>
60
58
  </footer>
61
59
  </body>
@@ -33,4 +33,4 @@ Use `start_process` once and then `read_process_output` or `interact_with_proces
33
33
 
34
34
  ## Out-of-scope requests
35
35
 
36
- Do not use ReMCP to access a computer the user has not paired or is not authorized to control. Do not ask for passwords, MFA codes, private keys, or other credentials through ReMCP tool inputs.
36
+ Do not use ReMCP to access a computer the user has not paired or is not authorized to control. Do not ask for or process passwords, MFA codes, private keys, API keys, payment-card data, protected health information, government identifiers, or other restricted credentials/data through ReMCP tools. If a requested file or command would expose those categories, ask the user to use a safer local workflow instead.
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/auth.mjs CHANGED
@@ -18,6 +18,7 @@ export async function verifyFirebaseToken(token) {
18
18
  uid,
19
19
  firebaseUid: payload.sub,
20
20
  email,
21
+ emailVerified: payload.email_verified === true,
21
22
  name: typeof payload.name === 'string' ? payload.name : '',
22
23
  };
23
24
  }
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({
@@ -38,7 +38,7 @@ export const config = Object.freeze({
38
38
  refreshTtlSeconds: Number(process.env.REFRESH_TTL_SECONDS || 2592000),
39
39
  rpcTimeoutMs: Number(process.env.RPC_TIMEOUT_MS || 120000),
40
40
  domainVerificationToken: (process.env.OPENAI_DOMAIN_VERIFICATION_TOKEN || '').trim(),
41
- domainVerificationPath: (process.env.OPENAI_DOMAIN_VERIFICATION_PATH || '/.well-known/openai-domain-verification').trim(),
41
+ domainVerificationPath: (process.env.OPENAI_DOMAIN_VERIFICATION_PATH || '/.well-known/openai-apps-challenge').trim(),
42
42
  firebase: {
43
43
  apiKey: required('FIREBASE_API_KEY'),
44
44
  authDomain: required('FIREBASE_AUTH_DOMAIN'),
package/src/db.mjs CHANGED
@@ -9,7 +9,7 @@ export const db = new DatabaseSync(join(config.dataDir, 'remcp.sqlite'));
9
9
  db.exec(`PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000;`);
10
10
  db.exec(`
11
11
  CREATE TABLE IF NOT EXISTS users (
12
- uid TEXT PRIMARY KEY, email TEXT, name TEXT, created_at INTEGER NOT NULL, last_login_at INTEGER NOT NULL
12
+ uid TEXT PRIMARY KEY, email TEXT, name TEXT, email_verified INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL, last_login_at INTEGER NOT NULL
13
13
  );
14
14
  CREATE TABLE IF NOT EXISTS pair_codes (
15
15
  code_hash TEXT PRIMARY KEY, uid TEXT NOT NULL REFERENCES users(uid) ON DELETE CASCADE,
@@ -34,12 +34,15 @@ CREATE TABLE IF NOT EXISTS oauth_refresh_tokens (
34
34
  scope TEXT NOT NULL, expires_at INTEGER NOT NULL, revoked_at INTEGER
35
35
  );
36
36
  `);
37
+ try { db.exec('ALTER TABLE users ADD COLUMN email_verified INTEGER NOT NULL DEFAULT 0'); } catch {}
37
38
 
38
- export function upsertUser({ uid, email = '', name = '' }) {
39
+ export const getUser = uid => db.prepare('SELECT * FROM users WHERE uid=?').get(uid);
40
+
41
+ export function upsertUser({ uid, email = '', name = '', emailVerified = false }) {
39
42
  const t = now();
40
- db.prepare(`INSERT INTO users(uid,email,name,created_at,last_login_at) VALUES(?,?,?,?,?)
41
- ON CONFLICT(uid) DO UPDATE SET email=excluded.email,name=excluded.name,last_login_at=excluded.last_login_at`)
42
- .run(uid, email, name, t, t);
43
+ db.prepare(`INSERT INTO users(uid,email,name,email_verified,created_at,last_login_at) VALUES(?,?,?,?,?,?)
44
+ ON CONFLICT(uid) DO UPDATE SET email=excluded.email,name=excluded.name,email_verified=excluded.email_verified,last_login_at=excluded.last_login_at`)
45
+ .run(uid, email, name, emailVerified ? 1 : 0, t, t);
43
46
  return db.prepare('SELECT * FROM users WHERE uid=?').get(uid);
44
47
  }
45
48
 
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,14 +50,14 @@ 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,
52
57
  readOnlyHint: tool.name === 'start_search' ? false : upstream.readOnlyHint === true,
53
58
  destructiveHint: destructive.has(tool.name),
54
59
  idempotentHint: upstream.idempotentHint === true,
55
- openWorldHint: tool.name === 'read_file' ? false : (openWorld.has(tool.name) || upstream.openWorldHint === true),
60
+ openWorldHint: openWorld.has(tool.name) || tool.name === 'read_file' || upstream.openWorldHint === true,
56
61
  },
57
62
  };
58
63
  }
@@ -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/oauth.mjs CHANGED
@@ -2,9 +2,9 @@ import crypto from 'node:crypto';
2
2
  import express from 'express';
3
3
  import { rateLimit } from 'express-rate-limit';
4
4
  import { config } from './config.mjs';
5
- import { verifyFirebaseToken, signMcpAccessToken } from './auth.mjs';
6
- import { createOauthCode, consumeOauthCode, getOauthClient, getRefreshToken, revokeRefreshToken, saveOauthClient, saveRefreshToken, upsertUser } from './db.mjs';
7
- import { normalizeScope, now, randomId, randomToken, sha256, validRedirectUri } from './util.mjs';
5
+ import { verifyFirebaseToken, signMcpAccessToken, verifyMcpAccessToken } from './auth.mjs';
6
+ import { createOauthCode, consumeOauthCode, getOauthClient, getRefreshToken, getUser, revokeRefreshToken, saveOauthClient, saveRefreshToken, upsertUser } from './db.mjs';
7
+ import { bearer, normalizeScope, now, randomId, randomToken, sha256, validRedirectUri } from './util.mjs';
8
8
 
9
9
  const CODE_TTL_MS = 5 * 60 * 1000;
10
10
  const router = express.Router();
@@ -18,12 +18,13 @@ function metadata() {
18
18
  authorization_endpoint: endpoint('/oauth/authorize'),
19
19
  token_endpoint: endpoint('/oauth/token'),
20
20
  registration_endpoint: endpoint('/oauth/register'),
21
+ userinfo_endpoint: endpoint('/oauth/userinfo'),
21
22
  revocation_endpoint: endpoint('/oauth/revoke'),
22
23
  response_types_supported: ['code'],
23
24
  grant_types_supported: ['authorization_code', 'refresh_token'],
24
25
  token_endpoint_auth_methods_supported: ['none'],
25
26
  code_challenge_methods_supported: ['S256'],
26
- scopes_supported: ['remcp:control', 'offline_access'],
27
+ scopes_supported: ['openid', 'email', 'remcp:control', 'offline_access'],
27
28
  };
28
29
  }
29
30
 
@@ -32,7 +33,7 @@ function protectedResource() {
32
33
  resource: endpoint('/mcp'),
33
34
  authorization_servers: [config.publicUrl],
34
35
  bearer_methods_supported: ['header'],
35
- scopes_supported: ['remcp:control', 'offline_access'],
36
+ scopes_supported: ['openid', 'email', 'remcp:control', 'offline_access'],
36
37
  resource_documentation: config.publicUrl,
37
38
  resource_policy_uri: endpoint('/privacy'),
38
39
  resource_tos_uri: endpoint('/terms'),
@@ -136,6 +137,24 @@ router.post('/oauth/token', tokenLimiter, async (req, res) => {
136
137
  }
137
138
  });
138
139
 
140
+
141
+ router.get('/oauth/userinfo', oauthLimiter, async (req, res) => {
142
+ noStore(res);
143
+ try {
144
+ const token = bearer(req);
145
+ if (!token) throw new Error('missing_token');
146
+ const auth = await verifyMcpAccessToken(token);
147
+ const scopes = new Set(auth.scope.split(/\s+/).filter(Boolean));
148
+ if (!scopes.has('openid') || !scopes.has('email')) throw new Error('insufficient_scope');
149
+ const user = getUser(auth.uid);
150
+ if (!user?.email || user.email_verified !== 1) throw new Error('verified_email_required');
151
+ res.json({ sub: auth.uid, email: user.email, email_verified: true, ...(user.name ? { name: user.name } : {}) });
152
+ } catch (error) {
153
+ res.setHeader('WWW-Authenticate', 'Bearer error="invalid_token"');
154
+ res.status(401).json({ error: 'invalid_token', error_description: error.message });
155
+ }
156
+ });
157
+
139
158
  router.post('/oauth/revoke', oauthLimiter, (req, res) => {
140
159
  const token = String(req.body?.token || '');
141
160
  if (token) revokeRefreshToken(sha256(token));