remote-access-mcp 1.0.6 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +354 -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 +123 -24
  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,11 +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
- const PKG = { name: 'remote-access-mcp', version: '1.0.0' };
8
+ import { AuditLog } from '../core/audit.js';
9
+ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
10
+ const PKG = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8'));
8
11
  // ---------------------------------------------------------------------------
9
- // help text
12
+ // help
10
13
  // ---------------------------------------------------------------------------
11
14
  const HELP = `${PKG.name} v${PKG.version} — turn any Linux server into an AI-agent-accessible machine
12
15
 
@@ -14,23 +17,48 @@ Usage:
14
17
  ramcp <command> [options]
15
18
 
16
19
  Commands:
17
- init Generate config + token (safe to re-run)
18
- start [--host H] [--port P] Run the gateway in the foreground
19
- url Print the MCP connector URL for chatbots
20
- status Check whether the systemd service is running
21
- token rotate Generate a new token (old one dies instantly)
22
- token show [--full] Show the current token (masked unless --full)
23
- policy Show the path policy + shell flag
24
- policy allow <path> Allow the AI access to a directory
25
- policy deny <path> Deny a directory (removes it from allowed)
26
- policy shell on|off Enable or disable shell execution
27
- service install Install systemd unit + nginx vhost
28
- service uninstall Remove systemd unit + nginx vhost
29
- service logs [-f] Tail gateway logs (journalctl wrapper)
30
- 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
31
58
 
32
59
  Options:
33
- -h, --help Show this help
60
+ -h, --help Show this help
61
+ --json Machine-readable output (where supported)
34
62
  `;
35
63
  function parseArgs(argv) {
36
64
  const [command = '', ...rest] = argv;
@@ -60,25 +88,47 @@ function parseArgs(argv) {
60
88
  }
61
89
  return { command, sub, flags, values };
62
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
+ }
63
108
  // ---------------------------------------------------------------------------
64
109
  // init
65
110
  // ---------------------------------------------------------------------------
66
- function cmdInit() {
111
+ function cmdInit(args) {
67
112
  const existing = fs.existsSync(configPath());
68
- let cfg;
69
- if (existing) {
70
- cfg = loadConfig();
71
- if (!cfg.token)
72
- cfg.token = generateToken();
73
- }
74
- else {
75
- cfg = {
76
- ...loadConfig(),
77
- token: generateToken(),
78
- };
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;
79
118
  }
80
- saveConfig(cfg);
81
- 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);
82
132
  console.log(`
83
133
  ___ _____
84
134
  | _ \\__ _ __ _ ___ _ _ |_ _|__ _ _ _ __ _ ___
@@ -86,144 +136,314 @@ function cmdInit() {
86
136
  |_|_\\__,_\\__, \\___|_| |_|\\___/_| |_| \\__,_\\___|
87
137
  |___/ ${PKG.name} v${PKG.version}
88
138
 
89
- ${existing ? '✔ Existing config found — token kept' : '✔ Config written to ' + configPath()}
90
- ✔ Token: ${masked} (full: ramcp token show --full)
91
- ✔ 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})
92
142
 
93
143
  Next steps:
94
- 1. ramcp start # run in foreground
95
- 2. ramcp service install # systemd + nginx (production)
96
- 3. ramcp policy allow /srv/myapp # let the AI touch your project
97
- 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
98
148
  `);
99
149
  }
100
150
  // ---------------------------------------------------------------------------
101
151
  // start
102
152
  // ---------------------------------------------------------------------------
103
153
  async function cmdStart(args) {
104
- const cfg = loadConfig();
105
- if (args.values.has('host'))
106
- cfg.host = args.values.get('host');
107
- if (args.values.has('port'))
108
- cfg.port = parseInt(args.values.get('port'), 10);
109
- if (!cfg.token) {
110
- console.error('No token. Run `ramcp init` first.');
111
- process.exit(1);
112
- }
113
154
  const { runServer } = await import('../server/run.js');
114
- 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
+ });
115
160
  }
116
161
  // ---------------------------------------------------------------------------
117
162
  // url
118
163
  // ---------------------------------------------------------------------------
119
- function cmdUrl() {
164
+ function cmdUrl(args) {
120
165
  const cfg = loadConfig();
121
- if (!cfg.token) {
122
- console.error('No token. Run `ramcp init` first.');
123
- process.exit(1);
124
- }
166
+ const t = findToken(cfg, args.sub[0]);
125
167
  const host = cfg.public_host || `${cfg.host}:${cfg.port}`;
126
168
  const scheme = cfg.public_host ? 'https' : 'http';
127
- console.log(`${scheme}://${host}/${cfg.token}${cfg.mcp_path}`);
169
+ console.log(`${scheme}://${host}/${t.token}${cfg.mcp_path}`);
128
170
  }
129
171
  // ---------------------------------------------------------------------------
130
- // token
172
+ // token management
131
173
  // ---------------------------------------------------------------------------
132
174
  function cmdToken(args) {
133
- const sub = args.sub[0];
134
175
  const cfg = loadConfig();
135
- if (sub === 'rotate') {
136
- const fresh = generateToken();
137
- cfg.token = fresh;
138
- saveConfig(cfg);
139
- console.log(`New token: ${fresh}`);
140
- console.log('Old token is dead. Update your chatbot connector now.');
141
- if (isSystemdInstalled()) {
142
- 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)}`);
143
188
  }
189
+ return;
144
190
  }
145
- else if (sub === 'show') {
146
- if (!cfg.token) {
147
- 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`);
148
199
  process.exit(1);
149
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]);
150
223
  if (args.flags.has('full'))
151
- console.log(cfg.token);
224
+ console.log(t.token);
152
225
  else
153
- 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;
154
228
  }
155
- else {
156
- console.log('Usage: ramcp token <rotate|show [--full]>');
157
- 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;
158
249
  }
250
+ console.log('Usage: ramcp token <list|add|show|rotate|revoke> …');
251
+ process.exit(1);
159
252
  }
160
253
  // ---------------------------------------------------------------------------
161
254
  // policy
162
255
  // ---------------------------------------------------------------------------
163
256
  function cmdPolicy(args) {
164
257
  const cfg = loadConfig();
165
- const sub = args.sub[0] || '';
166
- if (sub === 'allow' && args.sub[1]) {
167
- const real = resolveReal(args.sub[1]);
168
- if (!cfg.allowed_paths.includes(real))
169
- 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);
170
279
  saveConfig(cfg);
171
280
  console.log(`Allowed: ${real}`);
172
- reloadService();
281
+ restartHint();
173
282
  }
174
- else if (sub === 'deny' && args.sub[1]) {
175
- const real = resolveReal(args.sub[1]);
176
- cfg.allowed_paths = cfg.allowed_paths.filter((p) => p !== real);
177
- if (!cfg.denied_paths.includes(real))
178
- 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);
179
288
  saveConfig(cfg);
180
289
  console.log(`Denied: ${real}`);
181
- reloadService();
290
+ restartHint();
182
291
  }
183
- else if (sub === 'shell' && args.sub[1]) {
184
- const flag = args.sub[1].toLowerCase();
185
- if (flag !== 'on' && flag !== 'off') {
186
- console.error('Usage: ramcp policy shell on|off');
187
- process.exit(1);
188
- }
189
- 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');
190
294
  saveConfig(cfg);
191
- console.log(`Shell execution ${cfg.shell_enabled ? 'enabled' : 'disabled'}`);
192
- reloadService();
295
+ console.log(`Shell execution for "${t.name}": ${t.shell_enabled ? 'enabled' : 'disabled'}`);
296
+ restartHint();
193
297
  }
194
298
  else {
195
- console.log(`allowed: ${cfg.allowed_paths.length ? '\n ' + cfg.allowed_paths.join('\n ') : '(none)'}`);
196
- console.log(`denied: ${cfg.denied_paths.length ? '\n ' + cfg.denied_paths.join('\n ') : '(none)'}`);
197
- 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)'}`);
198
305
  }
199
306
  }
200
307
  // ---------------------------------------------------------------------------
201
- // service
308
+ // audit
202
309
  // ---------------------------------------------------------------------------
203
- const SERVICE_NAME = 'remote-access-mcp';
204
- const SERVICE_FILE = `/etc/systemd/system/${SERVICE_NAME}.service`;
205
- function isSystemdInstalled() {
206
- return fs.existsSync(SERVICE_FILE);
207
- }
208
- function serviceRunning() {
209
- try {
210
- const out = fs.readFileSync(`/proc/self/cgroup`, 'utf8');
211
- 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;
212
315
  }
213
- catch {
214
- 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
+ }
215
339
  }
340
+ audit.close();
216
341
  }
217
- function reloadService() {
218
- 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) {
219
372
  try {
220
- execFileSync('systemctl', ['restart', SERVICE_NAME], { stdio: 'ignore' });
221
- 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)}`);
222
379
  }
223
380
  catch {
224
- 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}`);
225
410
  }
226
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
+ }
227
447
  }
228
448
  function cmdService(args) {
229
449
  const sub = args.sub[0] || '';
@@ -247,7 +467,7 @@ function cmdService(args) {
247
467
  cmdStatus();
248
468
  }
249
469
  else {
250
- console.log('Usage: ramcp service <install|uninstall|logs [-f]|status>');
470
+ console.log('Usage: ramcp service <install [--domain D]|uninstall|logs [-f]|status>');
251
471
  process.exit(1);
252
472
  }
253
473
  }
@@ -259,7 +479,7 @@ function installService(args) {
259
479
  const domain = args.values.get('domain') || '';
260
480
  const cfg = loadConfig();
261
481
  const nodeBin = process.execPath;
262
- const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
482
+ const pkgRoot = PKG_ROOT;
263
483
  const unit = `[Unit]
264
484
  Description=Remote Access MCP Gateway
265
485
  After=network.target
@@ -312,7 +532,7 @@ WantedBy=multi-user.target
312
532
  try {
313
533
  execFileSync('nginx', ['-t']);
314
534
  execFileSync('systemctl', ['reload', 'nginx']);
315
- console.log(`✔ nginx vhost installed: ${domain} → 127.0.0.1:${cfg.port}`);
535
+ console.log(`✔ nginx vhost installed: ${domain} → ${cfg.host}:${cfg.port}`);
316
536
  }
317
537
  catch (e) {
318
538
  console.error(`nginx config test failed:\n${e.stdout || e.message}`);
@@ -328,8 +548,7 @@ function uninstallService() {
328
548
  console.error('service uninstall requires root (sudo).');
329
549
  process.exit(1);
330
550
  }
331
- const { existsSync, readFileSync } = fs;
332
- if (existsSync(SERVICE_FILE)) {
551
+ if (fs.existsSync(SERVICE_FILE)) {
333
552
  try {
334
553
  execFileSync('systemctl', ['disable', '--now', SERVICE_NAME]);
335
554
  }
@@ -341,13 +560,13 @@ function uninstallService() {
341
560
  else {
342
561
  console.log('Service not installed.');
343
562
  }
344
- // remove any nginx vhost that points at us
345
563
  const sitesDir = '/etc/nginx/sites-enabled';
346
- if (existsSync(sitesDir)) {
564
+ if (fs.existsSync(sitesDir)) {
565
+ const cfg = loadConfig();
347
566
  for (const f of fs.readdirSync(sitesDir)) {
348
567
  try {
349
- const content = readFileSync(path.join(sitesDir, f), 'utf8');
350
- 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}`)) {
351
570
  fs.unlinkSync(path.join(sitesDir, f));
352
571
  console.log(`✔ nginx vhost removed: ${f}`);
353
572
  }
@@ -368,17 +587,13 @@ function cmdStatus() {
368
587
  const installed = isSystemdInstalled();
369
588
  console.log(`service installed: ${installed ? 'yes' : 'no'}`);
370
589
  if (installed) {
371
- try {
372
- const out = execFileSync('systemctl', ['is-active', SERVICE_NAME], { encoding: 'utf8' }).trim();
373
- console.log(`service status: ${out}`);
374
- }
375
- catch (e) {
376
- console.log(`service status: ${e.stdout?.toString().trim() || 'unknown'}`);
377
- }
590
+ console.log(`service status: ${run('systemctl', ['is-active', SERVICE_NAME]).trim() || 'unknown'}`);
378
591
  }
379
592
  const cfg = loadConfig();
380
593
  console.log(`listen: ${cfg.host}:${cfg.port}`);
381
- 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'}`);
382
597
  }
383
598
  // ---------------------------------------------------------------------------
384
599
  // main
@@ -391,13 +606,13 @@ export async function main(argv) {
391
606
  }
392
607
  switch (args.command) {
393
608
  case 'init':
394
- cmdInit();
609
+ cmdInit(args);
395
610
  break;
396
611
  case 'start':
397
612
  await cmdStart(args);
398
613
  break;
399
614
  case 'url':
400
- cmdUrl();
615
+ cmdUrl(args);
401
616
  break;
402
617
  case 'token':
403
618
  cmdToken(args);
@@ -405,6 +620,12 @@ export async function main(argv) {
405
620
  case 'policy':
406
621
  cmdPolicy(args);
407
622
  break;
623
+ case 'audit':
624
+ cmdAudit(args);
625
+ break;
626
+ case 'doctor':
627
+ await cmdDoctor(args);
628
+ break;
408
629
  case 'service':
409
630
  cmdService(args);
410
631
  break;