remote-access-mcp 1.0.7 → 2.0.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.
Files changed (102) hide show
  1. package/README.fa.md +29 -70
  2. package/README.md +92 -65
  3. package/dist/cli/main.d.ts.map +1 -1
  4. package/dist/cli/main.js +352 -133
  5. package/dist/cli/main.js.map +1 -1
  6. package/dist/core/audit.d.ts +40 -0
  7. package/dist/core/audit.d.ts.map +1 -0
  8. package/dist/core/audit.js +96 -0
  9. package/dist/core/audit.js.map +1 -0
  10. package/dist/core/config.d.ts +37 -5
  11. package/dist/core/config.d.ts.map +1 -1
  12. package/dist/core/config.js +111 -34
  13. package/dist/core/config.js.map +1 -1
  14. package/dist/core/context.d.ts +23 -0
  15. package/dist/core/context.d.ts.map +1 -0
  16. package/dist/core/context.js +32 -0
  17. package/dist/core/context.js.map +1 -0
  18. package/dist/core/crypto.d.ts +6 -0
  19. package/dist/core/crypto.d.ts.map +1 -0
  20. package/dist/core/crypto.js +16 -0
  21. package/dist/core/crypto.js.map +1 -0
  22. package/dist/core/policy.d.ts +19 -0
  23. package/dist/core/policy.d.ts.map +1 -1
  24. package/dist/core/policy.js +57 -0
  25. package/dist/core/policy.js.map +1 -1
  26. package/dist/core/rate-limit.d.ts +14 -0
  27. package/dist/core/rate-limit.d.ts.map +1 -0
  28. package/dist/core/rate-limit.js +67 -0
  29. package/dist/core/rate-limit.js.map +1 -0
  30. package/dist/server/app.d.ts +18 -1
  31. package/dist/server/app.d.ts.map +1 -1
  32. package/dist/server/app.js +119 -25
  33. package/dist/server/app.js.map +1 -1
  34. package/dist/server/run.d.ts +5 -1
  35. package/dist/server/run.d.ts.map +1 -1
  36. package/dist/server/run.js +31 -8
  37. package/dist/server/run.js.map +1 -1
  38. package/dist/tools/filesystem.d.ts +3 -2
  39. package/dist/tools/filesystem.d.ts.map +1 -1
  40. package/dist/tools/filesystem.js +34 -23
  41. package/dist/tools/filesystem.js.map +1 -1
  42. package/dist/tools/git.d.ts +2 -2
  43. package/dist/tools/git.d.ts.map +1 -1
  44. package/dist/tools/git.js +33 -4
  45. package/dist/tools/git.js.map +1 -1
  46. package/dist/tools/http.d.ts +2 -1
  47. package/dist/tools/http.d.ts.map +1 -1
  48. package/dist/tools/http.js +47 -5
  49. package/dist/tools/http.js.map +1 -1
  50. package/dist/tools/index.d.ts +6 -3
  51. package/dist/tools/index.d.ts.map +1 -1
  52. package/dist/tools/index.js +29 -9
  53. package/dist/tools/index.js.map +1 -1
  54. package/dist/tools/logs.d.ts +4 -0
  55. package/dist/tools/logs.d.ts.map +1 -0
  56. package/dist/tools/logs.js +78 -0
  57. package/dist/tools/logs.js.map +1 -0
  58. package/dist/tools/packages.d.ts +4 -0
  59. package/dist/tools/packages.d.ts.map +1 -0
  60. package/dist/tools/packages.js +75 -0
  61. package/dist/tools/packages.js.map +1 -0
  62. package/dist/tools/planning.d.ts +4 -0
  63. package/dist/tools/planning.d.ts.map +1 -0
  64. package/dist/tools/planning.js +118 -0
  65. package/dist/tools/planning.js.map +1 -0
  66. package/dist/tools/policy.d.ts +7 -2
  67. package/dist/tools/policy.d.ts.map +1 -1
  68. package/dist/tools/policy.js +29 -18
  69. package/dist/tools/policy.js.map +1 -1
  70. package/dist/tools/project.d.ts +4 -0
  71. package/dist/tools/project.d.ts.map +1 -0
  72. package/dist/tools/project.js +141 -0
  73. package/dist/tools/project.js.map +1 -0
  74. package/dist/tools/schedule.d.ts +17 -0
  75. package/dist/tools/schedule.d.ts.map +1 -0
  76. package/dist/tools/schedule.js +113 -0
  77. package/dist/tools/schedule.js.map +1 -0
  78. package/dist/tools/security.d.ts +4 -0
  79. package/dist/tools/security.d.ts.map +1 -0
  80. package/dist/tools/security.js +103 -0
  81. package/dist/tools/security.js.map +1 -0
  82. package/dist/tools/services.d.ts +4 -0
  83. package/dist/tools/services.d.ts.map +1 -0
  84. package/dist/tools/services.js +56 -0
  85. package/dist/tools/services.js.map +1 -0
  86. package/dist/tools/shell.d.ts +2 -2
  87. package/dist/tools/shell.d.ts.map +1 -1
  88. package/dist/tools/shell.js +25 -10
  89. package/dist/tools/shell.js.map +1 -1
  90. package/dist/tools/sqlite.d.ts +2 -2
  91. package/dist/tools/sqlite.d.ts.map +1 -1
  92. package/dist/tools/sqlite.js +44 -9
  93. package/dist/tools/sqlite.js.map +1 -1
  94. package/dist/tools/system.d.ts +2 -1
  95. package/dist/tools/system.d.ts.map +1 -1
  96. package/dist/tools/system.js +6 -2
  97. package/dist/tools/system.js.map +1 -1
  98. package/dist/tools/web.d.ts +4 -0
  99. package/dist/tools/web.d.ts.map +1 -0
  100. package/dist/tools/web.js +77 -0
  101. package/dist/tools/web.js.map +1 -0
  102. package/package.json +1 -1
package/dist/cli/main.js CHANGED
@@ -2,13 +2,14 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { execFileSync } from 'node:child_process';
4
4
  import { fileURLToPath } from 'node:url';
5
- import { loadConfig, saveConfig, generateToken, configPath, } from '../core/config.js';
5
+ import net from 'node:net';
6
+ import { loadConfig, saveConfig, generateToken, newTokenRecord, configPath, configDir, primaryToken, } from '../core/config.js';
6
7
  import { resolveReal } from '../core/policy.js';
7
- // Read version from the installed package.json (single source of truth)
8
+ import { AuditLog } from '../core/audit.js';
8
9
  const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
9
10
  const PKG = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
10
11
  // ---------------------------------------------------------------------------
11
- // help text
12
+ // help
12
13
  // ---------------------------------------------------------------------------
13
14
  const HELP = `${PKG.name} v${PKG.version} — turn any Linux server into an AI-agent-accessible machine
14
15
 
@@ -16,23 +17,48 @@ Usage:
16
17
  ramcp <command> [options]
17
18
 
18
19
  Commands:
19
- init Generate config + token (safe to re-run)
20
- start [--host H] [--port P] Run the gateway in the foreground
21
- url Print the MCP connector URL for chatbots
22
- status Check whether the systemd service is running
23
- token rotate Generate a new token (old one dies instantly)
24
- token show [--full] Show the current token (masked unless --full)
25
- policy Show the path policy + shell flag
26
- policy allow <path> Allow the AI access to a directory
27
- policy deny <path> Deny a directory (removes it from allowed)
28
- policy shell on|off Enable or disable shell execution
29
- service install Install systemd unit + nginx vhost
30
- service uninstall Remove systemd unit + nginx vhost
31
- service logs [-f] Tail gateway logs (journalctl wrapper)
32
- version Print version
20
+ init Generate config + first token (safe to re-run)
21
+ start [--host H] [--port P]
22
+ [--read-only] Run the gateway in the foreground
23
+ url [--token ID|name] Print the MCP connector URL for chatbots
24
+ status Service status summary
25
+ doctor Diagnose token, port, nginx, DNS in one pass
26
+ version Print version
27
+
28
+ token list [--json] List tokens (fingerprints only)
29
+ token add --name N [opts] Create a token (see options below)
30
+ token show [ID|name] [--full] Show a token
31
+ token rotate [ID|name] Rotate a token (old one dies instantly)
32
+ token revoke ID|name Revoke (delete) a token
33
+ token add options:
34
+ --shell / --no-shell shell execution (default: off)
35
+ --paths a,b,c allowed paths (comma-separated)
36
+ --deny a,b denied paths
37
+ --scopes group,group tool groups (empty = all)
38
+ --read-only refuse mutating tools
39
+ --rpm N max requests per minute
40
+ --expires YYYY-MM-DD expiry date
41
+
42
+ policy [ID|name] Show a token's policy
43
+ policy allow <path> Allow a path (default token)
44
+ policy deny <path> Deny a path
45
+ policy shell on|off Enable/disable shell (default token)
46
+ policy readonly on|off Global read-only kill-switch
47
+
48
+ audit [query] [--tool T] [--since ISO] [--limit N]
49
+ [--verify] [--json] Query the tamper-evident audit log
50
+ audit chain Verify hash-chain integrity
51
+
52
+ service install [--domain D] Install systemd unit (+ nginx vhost)
53
+ service uninstall Remove service + nginx vhost
54
+ service logs [-f] Tail gateway logs
55
+ service status Detailed service check
56
+
57
+ schedule list List scheduled tasks
33
58
 
34
59
  Options:
35
- -h, --help Show this help
60
+ -h, --help Show this help
61
+ --json Machine-readable output (where supported)
36
62
  `;
37
63
  function parseArgs(argv) {
38
64
  const [command = '', ...rest] = argv;
@@ -62,25 +88,47 @@ function parseArgs(argv) {
62
88
  }
63
89
  return { command, sub, flags, values };
64
90
  }
91
+ const jsonOut = (args) => args.flags.has('json');
92
+ // ---------------------------------------------------------------------------
93
+ // token lookup helper
94
+ // ---------------------------------------------------------------------------
95
+ function findToken(cfg, idOrName) {
96
+ if (!idOrName)
97
+ return primaryToken(cfg);
98
+ const t = cfg.tokens.find(t => t.id === idOrName || t.name === idOrName);
99
+ if (!t) {
100
+ console.error(`Token "${idOrName}" not found. Try \`ramcp token list\`.`);
101
+ process.exit(1);
102
+ }
103
+ return t;
104
+ }
105
+ function maskToken(t) {
106
+ return t.slice(0, 6) + '…' + t.slice(-4);
107
+ }
65
108
  // ---------------------------------------------------------------------------
66
109
  // init
67
110
  // ---------------------------------------------------------------------------
68
- function cmdInit() {
111
+ function cmdInit(args) {
69
112
  const existing = fs.existsSync(configPath());
70
- let cfg;
71
- if (existing) {
72
- cfg = loadConfig();
73
- if (!cfg.token)
74
- cfg.token = generateToken();
75
- }
76
- else {
77
- cfg = {
78
- ...loadConfig(),
79
- token: generateToken(),
80
- };
113
+ const cfg = existing ? loadConfig() : null;
114
+ if (cfg && cfg.tokens.length) {
115
+ console.log('Config already exists with ' + cfg.tokens.length + ' token(s) — nothing to do.');
116
+ console.log('Add more tokens with: ramcp token add --name <name>');
117
+ return;
81
118
  }
82
- saveConfig(cfg);
83
- const masked = cfg.token.slice(0, 6) + '…' + cfg.token.slice(-4);
119
+ const fresh = cfg || {
120
+ host: '127.0.0.1',
121
+ port: 8765,
122
+ public_host: '',
123
+ mcp_path: '/mcp',
124
+ log_level: 'info',
125
+ audit: { enabled: true, db_path: path.join(configDir(), 'audit.db') },
126
+ read_only: false,
127
+ tokens: [],
128
+ };
129
+ const rec = newTokenRecord({ name: 'default' });
130
+ fresh.tokens.push(rec);
131
+ saveConfig(fresh);
84
132
  console.log(`
85
133
  ___ _____
86
134
  | _ \\__ _ __ _ ___ _ _ |_ _|__ _ _ _ __ _ ___
@@ -88,144 +136,314 @@ function cmdInit() {
88
136
  |_|_\\__,_\\__, \\___|_| |_|\\___/_| |_| \\__,_\\___|
89
137
  |___/ ${PKG.name} v${PKG.version}
90
138
 
91
- ${existing ? '✔ Existing config found token kept' : '✔ Config written to ' + configPath()}
92
- ✔ Token: ${masked} (full: ramcp token show --full)
93
- Config: ${configPath()}
139
+ ${existing ? '✔ Existing config migrated to v2' : '✔ Config written to ' + configPath()}
140
+ ✔ Token "default": ${maskToken(rec.token)} (full: ramcp token show default --full)
141
+ Audit log: ${fresh.audit.enabled ? 'enabled' : 'disabled'} (${fresh.audit.db_path})
94
142
 
95
143
  Next steps:
96
- 1. ramcp start # run in foreground
97
- 2. ramcp service install # systemd + nginx (production)
98
- 3. ramcp policy allow /srv/myapp # let the AI touch your project
99
- 4. ramcp url # connector URL for your chatbot
144
+ 1. ramcp start # run in foreground
145
+ 2. ramcp service install # systemd + nginx (production)
146
+ 3. ramcp policy allow /srv/myapp # let the AI touch your project
147
+ 4. ramcp url # connector URL for your chatbot
100
148
  `);
101
149
  }
102
150
  // ---------------------------------------------------------------------------
103
151
  // start
104
152
  // ---------------------------------------------------------------------------
105
153
  async function cmdStart(args) {
106
- const cfg = loadConfig();
107
- if (args.values.has('host'))
108
- cfg.host = args.values.get('host');
109
- if (args.values.has('port'))
110
- cfg.port = parseInt(args.values.get('port'), 10);
111
- if (!cfg.token) {
112
- console.error('No token. Run `ramcp init` first.');
113
- process.exit(1);
114
- }
115
154
  const { runServer } = await import('../server/run.js');
116
- await runServer();
155
+ await runServer({
156
+ host: args.values.get('host'),
157
+ port: args.values.has('port') ? parseInt(args.values.get('port'), 10) : undefined,
158
+ readOnly: args.flags.has('read-only'),
159
+ });
117
160
  }
118
161
  // ---------------------------------------------------------------------------
119
162
  // url
120
163
  // ---------------------------------------------------------------------------
121
- function cmdUrl() {
164
+ function cmdUrl(args) {
122
165
  const cfg = loadConfig();
123
- if (!cfg.token) {
124
- console.error('No token. Run `ramcp init` first.');
125
- process.exit(1);
126
- }
166
+ const t = findToken(cfg, args.sub[0]);
127
167
  const host = cfg.public_host || `${cfg.host}:${cfg.port}`;
128
168
  const scheme = cfg.public_host ? 'https' : 'http';
129
- console.log(`${scheme}://${host}/${cfg.token}${cfg.mcp_path}`);
169
+ console.log(`${scheme}://${host}/${t.token}${cfg.mcp_path}`);
130
170
  }
131
171
  // ---------------------------------------------------------------------------
132
- // token
172
+ // token management
133
173
  // ---------------------------------------------------------------------------
134
174
  function cmdToken(args) {
135
- const sub = args.sub[0];
136
175
  const cfg = loadConfig();
137
- if (sub === 'rotate') {
138
- const fresh = generateToken();
139
- cfg.token = fresh;
140
- saveConfig(cfg);
141
- console.log(`New token: ${fresh}`);
142
- console.log('Old token is dead. Update your chatbot connector now.');
143
- if (isSystemdInstalled()) {
144
- console.log('Restart the service to apply: systemctl restart remote-access-mcp');
176
+ const sub = args.sub[0];
177
+ if (sub === 'list') {
178
+ if (jsonOut(args)) {
179
+ console.log(JSON.stringify(cfg.tokens.map(t => ({
180
+ id: t.id, name: t.name, created: t.created, expires: t.expires || null,
181
+ scopes: t.scopes, read_only: !!t.read_only, shell: t.shell_enabled,
182
+ fingerprint: AuditLog.fingerprint(t.token),
183
+ })), null, 2));
184
+ return;
185
+ }
186
+ for (const t of cfg.tokens) {
187
+ console.log(`${t.id} ${t.name.padEnd(16)} shell:${t.shell_enabled ? 'on ' : 'off'} ro:${t.read_only ? 'yes' : 'no '} ${t.expires ? 'expires ' + t.expires + ' ' : ''}${t.scopes.length ? 'scopes: ' + t.scopes.join(',') + ' ' : ''}fp:${AuditLog.fingerprint(t.token)}`);
145
188
  }
189
+ return;
146
190
  }
147
- else if (sub === 'show') {
148
- if (!cfg.token) {
149
- console.error('No token. Run `ramcp init` first.');
191
+ if (sub === 'add') {
192
+ const name = args.values.get('name');
193
+ if (!name) {
194
+ console.error('--name is required');
195
+ process.exit(1);
196
+ }
197
+ if (cfg.tokens.some(t => t.name === name)) {
198
+ console.error(`Token "${name}" already exists`);
150
199
  process.exit(1);
151
200
  }
201
+ const rec = newTokenRecord({
202
+ name,
203
+ shell_enabled: args.flags.has('shell') ? true : args.flags.has('no-shell') ? false : undefined,
204
+ allowed_paths: args.values.get('paths')?.split(',').map(s => s.trim()).filter(Boolean),
205
+ denied_paths: args.values.get('deny')?.split(',').map(s => s.trim()).filter(Boolean),
206
+ scopes: args.values.get('scopes')?.split(',').map(s => s.trim()).filter(Boolean),
207
+ read_only: args.flags.has('read-only'),
208
+ max_requests_per_minute: args.values.has('rpm') ? parseInt(args.values.get('rpm'), 10) : undefined,
209
+ expires: args.values.get('expires'),
210
+ });
211
+ // resolve allowed paths to real paths
212
+ rec.allowed_paths = rec.allowed_paths.map(resolveReal);
213
+ rec.denied_paths = rec.denied_paths.map(resolveReal);
214
+ cfg.tokens.push(rec);
215
+ saveConfig(cfg);
216
+ console.log(`Token "${name}" created (${rec.id})`);
217
+ console.log(`URL: https://${cfg.public_host || cfg.host + ':' + cfg.port}/${rec.token}${cfg.mcp_path}`);
218
+ restartHint();
219
+ return;
220
+ }
221
+ if (sub === 'show') {
222
+ const t = findToken(cfg, args.sub[1]);
152
223
  if (args.flags.has('full'))
153
- console.log(cfg.token);
224
+ console.log(t.token);
154
225
  else
155
- console.log(cfg.token.slice(0, 6) + '…' + cfg.token.slice(-4) + ' (use --full for the complete token)');
226
+ console.log(`${t.name}: ${maskToken(t.token)} (use --full for the complete token)`);
227
+ return;
156
228
  }
157
- else {
158
- console.log('Usage: ramcp token <rotate|show [--full]>');
159
- process.exit(1);
229
+ if (sub === 'rotate') {
230
+ const t = findToken(cfg, args.sub[1]);
231
+ t.token = generateToken();
232
+ saveConfig(cfg);
233
+ console.log(`Token "${t.name}" rotated: ${t.token}`);
234
+ console.log('Old token is dead. Update your chatbot connector now.');
235
+ restartHint();
236
+ return;
237
+ }
238
+ if (sub === 'revoke') {
239
+ const t = findToken(cfg, args.sub[1]);
240
+ cfg.tokens = cfg.tokens.filter(x => x !== t);
241
+ if (!cfg.tokens.length) {
242
+ console.error('Cannot revoke the last token.');
243
+ process.exit(1);
244
+ }
245
+ saveConfig(cfg);
246
+ console.log(`Token "${t.name}" (${t.id}) revoked.`);
247
+ restartHint();
248
+ return;
160
249
  }
250
+ console.log('Usage: ramcp token <list|add|show|rotate|revoke> …');
251
+ process.exit(1);
161
252
  }
162
253
  // ---------------------------------------------------------------------------
163
254
  // policy
164
255
  // ---------------------------------------------------------------------------
165
256
  function cmdPolicy(args) {
166
257
  const cfg = loadConfig();
167
- const sub = args.sub[0] || '';
168
- if (sub === 'allow' && args.sub[1]) {
169
- const real = resolveReal(args.sub[1]);
170
- if (!cfg.allowed_paths.includes(real))
171
- cfg.allowed_paths.push(real);
258
+ // global readonly toggle has no token selector
259
+ if (args.sub[0] === 'readonly' && (args.sub[1] === 'on' || args.sub[1] === 'off')) {
260
+ cfg.read_only = args.sub[1] === 'on';
261
+ saveConfig(cfg);
262
+ console.log(`Global read-only: ${cfg.read_only ? 'ON — mutating tools refused' : 'off'}`);
263
+ restartHint();
264
+ return;
265
+ }
266
+ const t = findToken(cfg, args.sub[0] === 'allow' || args.sub[0] === 'deny' || args.sub[0] === 'shell' ? undefined : args.sub[0]);
267
+ const sub = args.sub[0] === 'allow' || args.sub[0] === 'deny' || args.sub[0] === 'shell' ? args.sub[0] : (args.sub[1] || 'show');
268
+ if (sub === 'allow' && args.values.size === 0 && !args.sub.find(s => s.startsWith('/'))) {
269
+ // ramcp policy allow <path> — path is in sub[1] (or sub[2] when selector given)
270
+ }
271
+ const pathArg = (() => {
272
+ const idx = args.sub.indexOf('allow') >= 0 ? args.sub.indexOf('allow') : args.sub.indexOf('deny');
273
+ return idx >= 0 ? args.sub[idx + 1] : undefined;
274
+ })();
275
+ if (sub === 'allow' && pathArg) {
276
+ const real = resolveReal(pathArg);
277
+ if (!t.allowed_paths.includes(real))
278
+ t.allowed_paths.push(real);
172
279
  saveConfig(cfg);
173
280
  console.log(`Allowed: ${real}`);
174
- reloadService();
281
+ restartHint();
175
282
  }
176
- else if (sub === 'deny' && args.sub[1]) {
177
- const real = resolveReal(args.sub[1]);
178
- cfg.allowed_paths = cfg.allowed_paths.filter((p) => p !== real);
179
- if (!cfg.denied_paths.includes(real))
180
- cfg.denied_paths.push(real);
283
+ else if (sub === 'deny' && pathArg) {
284
+ const real = resolveReal(pathArg);
285
+ t.allowed_paths = t.allowed_paths.filter((p) => p !== real);
286
+ if (!t.denied_paths.includes(real))
287
+ t.denied_paths.push(real);
181
288
  saveConfig(cfg);
182
289
  console.log(`Denied: ${real}`);
183
- reloadService();
290
+ restartHint();
184
291
  }
185
- else if (sub === 'shell' && args.sub[1]) {
186
- const flag = args.sub[1].toLowerCase();
187
- if (flag !== 'on' && flag !== 'off') {
188
- console.error('Usage: ramcp policy shell on|off');
189
- process.exit(1);
190
- }
191
- cfg.shell_enabled = flag === 'on';
292
+ else if (sub === 'shell' && (args.sub.includes('on') || args.sub.includes('off'))) {
293
+ t.shell_enabled = args.sub.includes('on');
192
294
  saveConfig(cfg);
193
- console.log(`Shell execution ${cfg.shell_enabled ? 'enabled' : 'disabled'}`);
194
- reloadService();
295
+ console.log(`Shell execution for "${t.name}": ${t.shell_enabled ? 'enabled' : 'disabled'}`);
296
+ restartHint();
195
297
  }
196
298
  else {
197
- console.log(`allowed: ${cfg.allowed_paths.length ? '\n ' + cfg.allowed_paths.join('\n ') : '(none)'}`);
198
- console.log(`denied: ${cfg.denied_paths.length ? '\n ' + cfg.denied_paths.join('\n ') : '(none)'}`);
199
- console.log(`shell: ${cfg.shell_enabled ? 'enabled' : 'disabled'}`);
299
+ console.log(`token: ${t.name} (${t.id})`);
300
+ console.log(`allowed: ${t.allowed_paths.length ? '\n ' + t.allowed_paths.join('\n ') : '(none)'}`);
301
+ console.log(`denied: ${t.denied_paths.length ? '\n ' + t.denied_paths.join('\n ') : '(none)'}`);
302
+ console.log(`shell: ${t.shell_enabled ? 'enabled' : 'disabled'}`);
303
+ console.log(`read_only: ${t.read_only ? 'yes' : 'no'}`);
304
+ console.log(`scopes: ${t.scopes.length ? t.scopes.join(', ') : '(all tools)'}`);
200
305
  }
201
306
  }
202
307
  // ---------------------------------------------------------------------------
203
- // service
308
+ // audit
204
309
  // ---------------------------------------------------------------------------
205
- const SERVICE_NAME = 'remote-access-mcp';
206
- const SERVICE_FILE = `/etc/systemd/system/${SERVICE_NAME}.service`;
207
- function isSystemdInstalled() {
208
- return fs.existsSync(SERVICE_FILE);
209
- }
210
- function serviceRunning() {
211
- try {
212
- const out = fs.readFileSync(`/proc/self/cgroup`, 'utf8');
213
- return false; // placeholder, real check below via systemctl
310
+ function cmdAudit(args) {
311
+ const cfg = loadConfig();
312
+ if (!cfg.audit.enabled) {
313
+ console.log('Audit log is disabled (audit.enabled = false).');
314
+ return;
214
315
  }
215
- catch {
216
- return false;
316
+ const audit = new AuditLog(cfg.audit.db_path);
317
+ if (args.sub[0] === 'chain' || args.flags.has('verify')) {
318
+ const tampered = audit.verify();
319
+ console.log(tampered === null ? '✔ hash chain intact' : `✖ TAMPERED — first bad row id: ${tampered}`);
320
+ process.exit(tampered === null ? 0 : 2);
321
+ }
322
+ const rows = audit.query({
323
+ tool: args.values.get('tool'),
324
+ since: args.values.has('since') ? new Date(args.values.get('since')).getTime() : undefined,
325
+ limit: args.values.has('limit') ? parseInt(args.values.get('limit'), 10) : 50,
326
+ });
327
+ if (jsonOut(args)) {
328
+ console.log(JSON.stringify(rows, null, 2));
329
+ }
330
+ else {
331
+ if (!rows.length) {
332
+ console.log('(no audit entries)');
333
+ return;
334
+ }
335
+ for (const r of rows) {
336
+ const d = new Date(r.ts).toISOString().slice(0, 19);
337
+ console.log(`${d} fp:${r.token_fingerprint} ${r.tool.padEnd(20)} ${r.is_error ? 'ERR ' : 'ok '} ${r.duration_ms}ms`);
338
+ }
217
339
  }
340
+ audit.close();
218
341
  }
219
- function reloadService() {
220
- if (isSystemdInstalled()) {
342
+ // ---------------------------------------------------------------------------
343
+ // doctor
344
+ // ---------------------------------------------------------------------------
345
+ async function cmdDoctor(args) {
346
+ const results = [];
347
+ const ok = (name, msg) => results.push([`✔ ${name}`, msg]);
348
+ const bad = (name, msg) => results.push([`✖ ${name}`, msg]);
349
+ const warn = (name, msg) => results.push([`⚠ ${name}`, msg]);
350
+ const cfg = loadConfig();
351
+ // 1. token
352
+ if (cfg.tokens.length)
353
+ ok('tokens', `${cfg.tokens.length} configured, primary: ${cfg.tokens[0].name}`);
354
+ else
355
+ bad('tokens', 'none — run `ramcp init`');
356
+ // 2. bind port free?
357
+ const inUse = await new Promise((resolve) => {
358
+ const srv = net.createServer();
359
+ srv.once('error', () => resolve(true));
360
+ srv.once('listening', () => { srv.close(() => resolve(false)); });
361
+ srv.listen(cfg.port, cfg.host);
362
+ });
363
+ const serviceActive = fs.existsSync(SERVICE_FILE) && run('systemctl', ['is-active', SERVICE_NAME]).trim() === 'active';
364
+ if (inUse) {
365
+ serviceActive ? ok('port', `${cfg.host}:${cfg.port} in use by the running service`) : warn('port', `${cfg.host}:${cfg.port} in use by ANOTHER process`);
366
+ }
367
+ else {
368
+ serviceActive ? warn('port', `port free but service claims active — crashed?`) : warn('port', `not listening — start with \`ramcp service install\` or \`ramcp start\``);
369
+ }
370
+ // 3. gateway answers?
371
+ if (!inUse || serviceActive) {
221
372
  try {
222
- execFileSync('systemctl', ['restart', SERVICE_NAME], { stdio: 'ignore' });
223
- console.log('Service restarted to apply changes.');
373
+ const resp = await fetch(`http://${cfg.host}:${cfg.port}/health`);
374
+ const body = (await resp.json());
375
+ if (body.status === 'ok')
376
+ ok('gateway', `healthy (v${body.version})`);
377
+ else
378
+ bad('gateway', `unexpected: ${JSON.stringify(body)}`);
224
379
  }
225
380
  catch {
226
- console.log('Note: service did not restart (not root?). Reboot or restart manually.');
381
+ bad('gateway', `no answer on ${cfg.host}:${cfg.port}/health`);
382
+ }
383
+ }
384
+ // 4. nginx vhost?
385
+ if (cfg.public_host) {
386
+ const vhost = `/etc/nginx/sites-enabled/${cfg.public_host}`;
387
+ if (fs.existsSync(vhost)) {
388
+ const conf = fs.readFileSync(vhost, 'utf8');
389
+ if (conf.includes(`proxy_pass http://${cfg.host}:${cfg.port}`)) {
390
+ ok('nginx', `vhost for ${cfg.public_host} → ${cfg.host}:${cfg.port}`);
391
+ }
392
+ else {
393
+ warn('nginx', `${vhost} exists but does not proxy to ${cfg.host}:${cfg.port}`);
394
+ }
395
+ }
396
+ else {
397
+ warn('nginx', `no vhost at ${vhost} — run \`ramcp service install --domain ${cfg.public_host}\``);
398
+ }
399
+ // 5. public DNS resolves + Cloudflare edge reachable?
400
+ try {
401
+ const resp = await fetch(`https://${cfg.public_host}/health`, { signal: AbortSignal.timeout(8000) });
402
+ const body = (await resp.json());
403
+ if (body.status === 'ok')
404
+ ok('public', `https://${cfg.public_host} healthy end-to-end`);
405
+ else
406
+ warn('public', `got status ${resp.status} from edge`);
407
+ }
408
+ catch (e) {
409
+ warn('public', `https://${cfg.public_host} unreachable: ${e.message}`);
227
410
  }
228
411
  }
412
+ // 6. audit chain
413
+ if (cfg.audit.enabled && fs.existsSync(cfg.audit.db_path)) {
414
+ const audit = new AuditLog(cfg.audit.db_path);
415
+ const tampered = audit.verify();
416
+ tampered === null ? ok('audit', 'hash chain intact') : bad('audit', `tampered at row ${tampered}`);
417
+ audit.close();
418
+ }
419
+ else if (cfg.audit.enabled) {
420
+ warn('audit', 'enabled but db not created yet');
421
+ }
422
+ for (const [name, msg] of results)
423
+ console.log(`${name.padEnd(14)} ${msg}`);
424
+ const hasBad = results.some(r => r[0].startsWith('✖'));
425
+ process.exitCode = hasBad ? 1 : 0;
426
+ }
427
+ function run(cmd, args) {
428
+ try {
429
+ return execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
430
+ }
431
+ catch {
432
+ return '';
433
+ }
434
+ }
435
+ // ---------------------------------------------------------------------------
436
+ // service
437
+ // ---------------------------------------------------------------------------
438
+ const SERVICE_NAME = 'remote-access-mcp';
439
+ const SERVICE_FILE = `/etc/systemd/system/${SERVICE_NAME}.service`;
440
+ function isSystemdInstalled() {
441
+ return fs.existsSync(SERVICE_FILE);
442
+ }
443
+ function restartHint() {
444
+ if (isSystemdInstalled()) {
445
+ console.log('(service restarts automatically on next request — config hot-reloads)');
446
+ }
229
447
  }
230
448
  function cmdService(args) {
231
449
  const sub = args.sub[0] || '';
@@ -249,7 +467,7 @@ function cmdService(args) {
249
467
  cmdStatus();
250
468
  }
251
469
  else {
252
- console.log('Usage: ramcp service <install|uninstall|logs [-f]|status>');
470
+ console.log('Usage: ramcp service <install [--domain D]|uninstall|logs [-f]|status>');
253
471
  process.exit(1);
254
472
  }
255
473
  }
@@ -261,7 +479,7 @@ function installService(args) {
261
479
  const domain = args.values.get('domain') || '';
262
480
  const cfg = loadConfig();
263
481
  const nodeBin = process.execPath;
264
- const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
482
+ const pkgRoot = PKG_ROOT;
265
483
  const unit = `[Unit]
266
484
  Description=Remote Access MCP Gateway
267
485
  After=network.target
@@ -314,7 +532,7 @@ WantedBy=multi-user.target
314
532
  try {
315
533
  execFileSync('nginx', ['-t']);
316
534
  execFileSync('systemctl', ['reload', 'nginx']);
317
- console.log(`✔ nginx vhost installed: ${domain} → 127.0.0.1:${cfg.port}`);
535
+ console.log(`✔ nginx vhost installed: ${domain} → ${cfg.host}:${cfg.port}`);
318
536
  }
319
537
  catch (e) {
320
538
  console.error(`nginx config test failed:\n${e.stdout || e.message}`);
@@ -330,8 +548,7 @@ function uninstallService() {
330
548
  console.error('service uninstall requires root (sudo).');
331
549
  process.exit(1);
332
550
  }
333
- const { existsSync, readFileSync } = fs;
334
- if (existsSync(SERVICE_FILE)) {
551
+ if (fs.existsSync(SERVICE_FILE)) {
335
552
  try {
336
553
  execFileSync('systemctl', ['disable', '--now', SERVICE_NAME]);
337
554
  }
@@ -343,13 +560,13 @@ function uninstallService() {
343
560
  else {
344
561
  console.log('Service not installed.');
345
562
  }
346
- // remove any nginx vhost that points at us
347
563
  const sitesDir = '/etc/nginx/sites-enabled';
348
- if (existsSync(sitesDir)) {
564
+ if (fs.existsSync(sitesDir)) {
565
+ const cfg = loadConfig();
349
566
  for (const f of fs.readdirSync(sitesDir)) {
350
567
  try {
351
- const content = readFileSync(path.join(sitesDir, f), 'utf8');
352
- if (content.includes(`proxy_pass http://127.0.0.1:${loadConfig().port}`)) {
568
+ const content = fs.readFileSync(path.join(sitesDir, f), 'utf8');
569
+ if (content.includes(`proxy_pass http://${cfg.host}:${cfg.port}`)) {
353
570
  fs.unlinkSync(path.join(sitesDir, f));
354
571
  console.log(`✔ nginx vhost removed: ${f}`);
355
572
  }
@@ -370,17 +587,13 @@ function cmdStatus() {
370
587
  const installed = isSystemdInstalled();
371
588
  console.log(`service installed: ${installed ? 'yes' : 'no'}`);
372
589
  if (installed) {
373
- try {
374
- const out = execFileSync('systemctl', ['is-active', SERVICE_NAME], { encoding: 'utf8' }).trim();
375
- console.log(`service status: ${out}`);
376
- }
377
- catch (e) {
378
- console.log(`service status: ${e.stdout?.toString().trim() || 'unknown'}`);
379
- }
590
+ console.log(`service status: ${run('systemctl', ['is-active', SERVICE_NAME]).trim() || 'unknown'}`);
380
591
  }
381
592
  const cfg = loadConfig();
382
593
  console.log(`listen: ${cfg.host}:${cfg.port}`);
383
- console.log(`token: ${cfg.token ? cfg.token.slice(0, 6) + '…' : '(none — run ramcp init)'}`);
594
+ console.log(`tokens: ${cfg.tokens.length}`);
595
+ console.log(`audit: ${cfg.audit.enabled ? 'on' : 'off'}`);
596
+ console.log(`read_only: ${cfg.read_only ? 'on' : 'off'}`);
384
597
  }
385
598
  // ---------------------------------------------------------------------------
386
599
  // main
@@ -393,13 +606,13 @@ export async function main(argv) {
393
606
  }
394
607
  switch (args.command) {
395
608
  case 'init':
396
- cmdInit();
609
+ cmdInit(args);
397
610
  break;
398
611
  case 'start':
399
612
  await cmdStart(args);
400
613
  break;
401
614
  case 'url':
402
- cmdUrl();
615
+ cmdUrl(args);
403
616
  break;
404
617
  case 'token':
405
618
  cmdToken(args);
@@ -407,6 +620,12 @@ export async function main(argv) {
407
620
  case 'policy':
408
621
  cmdPolicy(args);
409
622
  break;
623
+ case 'audit':
624
+ cmdAudit(args);
625
+ break;
626
+ case 'doctor':
627
+ await cmdDoctor(args);
628
+ break;
410
629
  case 'service':
411
630
  cmdService(args);
412
631
  break;