@vdoninja/mcp 0.4.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.
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { spawnSync } = require('node:child_process');
7
+
8
+ const PRESETS = Object.freeze({
9
+ full: Object.freeze({
10
+ serverArgs: [],
11
+ description: 'Full tool surface (core + file + state).'
12
+ }),
13
+ core: Object.freeze({
14
+ serverArgs: ['--tool-profile', 'core'],
15
+ description: 'Core-only tool surface for messaging and sync.'
16
+ }),
17
+ file: Object.freeze({
18
+ serverArgs: ['--tool-profile', 'file'],
19
+ description: 'Core + file transfer tools.'
20
+ }),
21
+ state: Object.freeze({
22
+ serverArgs: ['--tool-profile', 'state'],
23
+ description: 'Core + shared state tools.'
24
+ }),
25
+ 'secure-core': Object.freeze({
26
+ serverArgs: ['--tool-profile', 'core', '--enforce-join-token', 'true', '--require-session-mac', 'true'],
27
+ description: 'Core profile plus join-token enforcement and session MAC requirement.'
28
+ }),
29
+ 'secure-full': Object.freeze({
30
+ serverArgs: ['--tool-profile', 'full', '--enforce-join-token', 'true', '--require-session-mac', 'true'],
31
+ description: 'Full profile plus join-token enforcement and session MAC requirement.'
32
+ })
33
+ });
34
+
35
+ function usage() {
36
+ const presetList = Object.keys(PRESETS)
37
+ .map((name) => ` - ${name}: ${PRESETS[name].description}`)
38
+ .join('\n');
39
+ const text = `
40
+ Usage:
41
+ vdon-mcp-install [options]
42
+
43
+ Options:
44
+ --uninstall Remove MCP server entries instead of installing.
45
+ --name <name> Server name. Default: vdo-ninja-mcp
46
+ --server-script <path> Path to MCP server script. Default: bundled scripts/vdo-mcp-server.js
47
+ --node-cmd <path_or_name> Node executable used in registered command. Default: current node
48
+ --preset <name> Registration preset. Default: full
49
+ --server-arg <value> Extra server argument (repeatable).
50
+ --codex-only Only apply Codex MCP configuration.
51
+ --claude-only Only apply Claude MCP configuration.
52
+ --claude-scope <scope> Claude scope: local|user|project. Default: local
53
+ --dry-run Print actions without executing.
54
+ -h, --help Show this help.
55
+
56
+ Presets:
57
+ ${presetList}
58
+ `;
59
+ process.stdout.write(text.trim() + '\n');
60
+ }
61
+
62
+ function parseArgs(argv) {
63
+ const out = {
64
+ action: 'install',
65
+ name: 'vdo-ninja-mcp',
66
+ serverScript: path.resolve(__dirname, 'vdo-mcp-server.js'),
67
+ nodeCmd: process.execPath,
68
+ preset: 'full',
69
+ serverArgs: [],
70
+ codex: true,
71
+ claude: true,
72
+ claudeScope: 'local',
73
+ dryRun: false
74
+ };
75
+
76
+ for (let i = 0; i < argv.length; i += 1) {
77
+ const arg = argv[i];
78
+ if (arg === '--uninstall') {
79
+ out.action = 'uninstall';
80
+ continue;
81
+ }
82
+ if (arg === '--name') {
83
+ out.name = argv[++i];
84
+ continue;
85
+ }
86
+ if (arg === '--server-script') {
87
+ out.serverScript = path.resolve(argv[++i]);
88
+ continue;
89
+ }
90
+ if (arg === '--node-cmd') {
91
+ out.nodeCmd = argv[++i];
92
+ continue;
93
+ }
94
+ if (arg === '--preset') {
95
+ out.preset = String(argv[++i] || '').trim().toLowerCase();
96
+ continue;
97
+ }
98
+ if (arg === '--server-arg') {
99
+ const value = argv[++i];
100
+ if (value === undefined) {
101
+ throw new Error('--server-arg requires a value');
102
+ }
103
+ out.serverArgs.push(String(value));
104
+ continue;
105
+ }
106
+ if (arg === '--codex-only') {
107
+ out.claude = false;
108
+ out.codex = true;
109
+ continue;
110
+ }
111
+ if (arg === '--claude-only') {
112
+ out.codex = false;
113
+ out.claude = true;
114
+ continue;
115
+ }
116
+ if (arg === '--claude-scope') {
117
+ out.claudeScope = String(argv[++i] || '').trim().toLowerCase();
118
+ continue;
119
+ }
120
+ if (arg === '--dry-run') {
121
+ out.dryRun = true;
122
+ continue;
123
+ }
124
+ if (arg === '--help' || arg === '-h') {
125
+ usage();
126
+ process.exit(0);
127
+ }
128
+ throw new Error(`Unknown argument: ${arg}`);
129
+ }
130
+
131
+ if (!out.codex && !out.claude) {
132
+ throw new Error('No target selected (Codex/Claude).');
133
+ }
134
+ if (!out.name || !String(out.name).trim()) {
135
+ throw new Error('Server name is required.');
136
+ }
137
+ if (!['local', 'user', 'project'].includes(out.claudeScope)) {
138
+ throw new Error(`Invalid --claude-scope: ${out.claudeScope}`);
139
+ }
140
+ if (!PRESETS[out.preset]) {
141
+ throw new Error(`Invalid --preset: ${out.preset}`);
142
+ }
143
+ return out;
144
+ }
145
+
146
+ function runCommand(command, args, options = {}) {
147
+ const cmdLabel = `${command} ${args.join(' ')}`;
148
+ if (options.dryRun) {
149
+ process.stdout.write(`[dry-run] ${cmdLabel}\n`);
150
+ return { ok: true, code: 0, stdout: '', stderr: '' };
151
+ }
152
+
153
+ const result = spawnSync(command, args, {
154
+ encoding: 'utf8',
155
+ env: options.env || process.env,
156
+ cwd: options.cwd || process.cwd()
157
+ });
158
+
159
+ const code = Number.isInteger(result.status) ? result.status : 1;
160
+ const stdout = result.stdout || '';
161
+ const stderr = result.stderr || '';
162
+ if (!options.quiet && stdout.trim()) process.stdout.write(stdout);
163
+ if (!options.quiet && stderr.trim()) process.stderr.write(stderr);
164
+ return {
165
+ ok: code === 0,
166
+ code,
167
+ stdout,
168
+ stderr
169
+ };
170
+ }
171
+
172
+ function assertBinaryExists(name, options) {
173
+ const check = runCommand(name, ['--version'], {
174
+ dryRun: options.dryRun,
175
+ quiet: true,
176
+ env: options.env,
177
+ cwd: options.cwd
178
+ });
179
+ if (!check.ok) {
180
+ throw new Error(`Required command not found or not working: ${name}`);
181
+ }
182
+ }
183
+
184
+ function codexServerExists(name, options) {
185
+ if (options.dryRun) return false;
186
+ const result = runCommand('codex', ['mcp', 'get', name, '--json'], {
187
+ dryRun: options.dryRun,
188
+ quiet: true,
189
+ env: options.env,
190
+ cwd: options.cwd
191
+ });
192
+ return result.ok;
193
+ }
194
+
195
+ function claudeServerExists(name, options) {
196
+ if (options.dryRun) return false;
197
+ const result = runCommand('claude', ['mcp', 'get', name], {
198
+ dryRun: options.dryRun,
199
+ quiet: true,
200
+ env: options.env,
201
+ cwd: options.cwd
202
+ });
203
+ return result.ok;
204
+ }
205
+
206
+ function installCodex(config, options) {
207
+ process.stdout.write(`Installing Codex MCP server '${config.name}'...\n`);
208
+ if (codexServerExists(config.name, options)) {
209
+ runCommand('codex', ['mcp', 'remove', config.name], options);
210
+ }
211
+ const add = runCommand('codex', ['mcp', 'add', config.name, '--', config.nodeCmd, config.serverScript, ...config.serverArgs], options);
212
+ if (!add.ok) throw new Error(`Failed to add Codex MCP server '${config.name}'.`);
213
+ runCommand('codex', ['mcp', 'get', config.name, '--json'], options);
214
+ }
215
+
216
+ function uninstallCodex(config, options) {
217
+ process.stdout.write(`Removing Codex MCP server '${config.name}'...\n`);
218
+ if (!codexServerExists(config.name, options)) {
219
+ process.stdout.write('Codex MCP server not found; skipping.\n');
220
+ return;
221
+ }
222
+ const remove = runCommand('codex', ['mcp', 'remove', config.name], options);
223
+ if (!remove.ok) throw new Error(`Failed to remove Codex MCP server '${config.name}'.`);
224
+ }
225
+
226
+ function installClaude(config, options) {
227
+ process.stdout.write(`Installing Claude MCP server '${config.name}' (scope=${config.claudeScope})...\n`);
228
+ if (claudeServerExists(config.name, options)) {
229
+ runCommand('claude', ['mcp', 'remove', config.name], options);
230
+ }
231
+ const add = runCommand(
232
+ 'claude',
233
+ ['mcp', 'add', '-s', config.claudeScope, config.name, '--', config.nodeCmd, config.serverScript, ...config.serverArgs],
234
+ options
235
+ );
236
+ if (!add.ok) throw new Error(`Failed to add Claude MCP server '${config.name}'.`);
237
+ runCommand('claude', ['mcp', 'get', config.name], options);
238
+ }
239
+
240
+ function uninstallClaude(config, options) {
241
+ process.stdout.write(`Removing Claude MCP server '${config.name}'...\n`);
242
+ if (!claudeServerExists(config.name, options)) {
243
+ process.stdout.write('Claude MCP server not found; skipping.\n');
244
+ return;
245
+ }
246
+ const remove = runCommand('claude', ['mcp', 'remove', config.name], options);
247
+ if (!remove.ok) throw new Error(`Failed to remove Claude MCP server '${config.name}'.`);
248
+ }
249
+
250
+ function ensureInputs(config) {
251
+ if (!fs.existsSync(config.serverScript)) {
252
+ throw new Error(`Server script not found: ${config.serverScript}`);
253
+ }
254
+ }
255
+
256
+ function applyPreset(config) {
257
+ const preset = PRESETS[config.preset];
258
+ const mergedServerArgs = [...preset.serverArgs, ...config.serverArgs];
259
+ return {
260
+ ...config,
261
+ serverArgs: mergedServerArgs
262
+ };
263
+ }
264
+
265
+ function main() {
266
+ let config = parseArgs(process.argv.slice(2));
267
+ config = applyPreset(config);
268
+ ensureInputs(config);
269
+
270
+ const options = {
271
+ dryRun: config.dryRun,
272
+ env: process.env,
273
+ cwd: process.cwd()
274
+ };
275
+
276
+ process.stdout.write(`Using preset '${config.preset}' with server args: ${config.serverArgs.join(' ') || '(none)'}\n`);
277
+ if (config.preset.startsWith('secure') && !process.env.VDON_MCP_JOIN_TOKEN_SECRET) {
278
+ process.stdout.write('Warning: secure preset selected but VDON_MCP_JOIN_TOKEN_SECRET is not set in this shell.\n');
279
+ }
280
+
281
+ if (config.codex) assertBinaryExists('codex', options);
282
+ if (config.claude) assertBinaryExists('claude', options);
283
+
284
+ if (config.action === 'install') {
285
+ if (config.codex) installCodex(config, options);
286
+ if (config.claude) installClaude(config, options);
287
+ } else {
288
+ if (config.codex) uninstallCodex(config, options);
289
+ if (config.claude) uninstallClaude(config, options);
290
+ }
291
+
292
+ process.stdout.write('\nDone.\n');
293
+ }
294
+
295
+ if (require.main === module) {
296
+ try {
297
+ main();
298
+ } catch (error) {
299
+ process.stderr.write(`${error.message}\n`);
300
+ process.exit(1);
301
+ }
302
+ }