@nado-language/mcp 0.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.
@@ -0,0 +1,521 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { spawn, spawnSync } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
10
+ const sourceRoot = path.resolve(scriptDir, '..');
11
+ const packageDistDir = scriptDir;
12
+ const serverName = 'nado-language';
13
+ const opencodeSchema = 'https://opencode.ai/config.json';
14
+
15
+ const serverPath = firstExisting([
16
+ path.join(packageDistDir, 'nado-language-server.mjs'),
17
+ path.join(sourceRoot, 'mcp', 'nado-language-server.mjs'),
18
+ ]);
19
+ const authPath = firstExisting([
20
+ path.join(packageDistDir, 'nado-mcp-auth.mjs'),
21
+ path.join(sourceRoot, 'scripts', 'nado-mcp-auth.mjs'),
22
+ ]);
23
+ const probePath = firstExisting([
24
+ path.join(packageDistDir, 'probe-nado-mcp.mjs'),
25
+ path.join(sourceRoot, 'scripts', 'probe-nado-mcp.mjs'),
26
+ ]);
27
+
28
+ const command = process.argv[2] || 'help';
29
+ const args = process.argv.slice(3);
30
+
31
+ try {
32
+ if (command === 'help' || command === '--help' || command === '-h') {
33
+ printHelp();
34
+ } else if (command === 'server') {
35
+ await runNode(serverPath, args, { stdio: 'inherit' });
36
+ } else if (command === 'login') {
37
+ await runAuth(['login', ...args]);
38
+ } else if (command === 'status') {
39
+ await runAuth(['status', ...args]);
40
+ } else if (command === 'logout') {
41
+ await runAuth(['logout', ...args]);
42
+ } else if (command === 'auth') {
43
+ await runAuth(args.length > 0 ? args : ['login']);
44
+ } else if (command === 'probe') {
45
+ await runNode(probePath, args, { stdio: 'inherit' });
46
+ } else if (command === 'setup') {
47
+ const parsed = splitClientAndSetupOptions(args);
48
+ await setup(parsed.client, parsed.setupOptions);
49
+ } else if (command === 'connect' || command === 'install') {
50
+ const parsed = splitClientAndSetupOptions(args);
51
+ await setup(parsed.client, parsed.setupOptions, { loginAfter: true });
52
+ await runAuth(['login', ...parsed.rest]);
53
+ } else if (command === 'config') {
54
+ printConfig(args[0] || 'all');
55
+ } else if (command === 'doctor') {
56
+ doctor();
57
+ } else {
58
+ throw new Error(`Unknown command: ${command}`);
59
+ }
60
+ } catch (error) {
61
+ console.error(`nado-mcp failed: ${error instanceof Error ? error.message : String(error)}`);
62
+ process.exitCode = 1;
63
+ }
64
+
65
+ async function runAuth(authArgs) {
66
+ const nextArgs = [...authArgs];
67
+ if (!hasOption(nextArgs, '--auth-file') && !process.env.NADO_MCP_AUTH_ENV_FILE) {
68
+ nextArgs.push('--auth-file', defaultUserAuthEnvFile());
69
+ }
70
+ await runNode(authPath, nextArgs, { stdio: 'inherit' });
71
+ }
72
+
73
+ async function setup(client, options = {}, flow = {}) {
74
+ const normalized = String(client || '').toLowerCase();
75
+ if (normalized === 'codex') {
76
+ setupCodex(flow);
77
+ return true;
78
+ }
79
+ if (normalized === 'claude' || normalized === 'claude-desktop') {
80
+ setupClaudeDesktop(options, flow);
81
+ return true;
82
+ }
83
+ if (normalized === 'opencode' || normalized === 'open-code') {
84
+ setupOpenCode(options, flow);
85
+ return true;
86
+ }
87
+ if (normalized === 'mcp-json' || normalized === 'json-file') {
88
+ if (options.configFile) {
89
+ setupMcpServersJson(options.configFile, flow);
90
+ return true;
91
+ }
92
+ printMcpServersSetup('mcp-json');
93
+ return false;
94
+ }
95
+ if (normalized === 'generic' || normalized === 'manual' || normalized === 'stdio' || normalized === 'json' || normalized === 'mcp') {
96
+ if (options.configFile) {
97
+ setupMcpServersJson(options.configFile, flow);
98
+ return true;
99
+ }
100
+ printGenericSetup();
101
+ return false;
102
+ }
103
+
104
+ if (options.configFile) {
105
+ console.log(`No built-in config writer for "${client}". Using generic mcpServers JSON at ${options.configFile}.`);
106
+ setupMcpServersJson(options.configFile, flow);
107
+ return true;
108
+ }
109
+
110
+ printUnknownClientSetup(client);
111
+ return false;
112
+ }
113
+
114
+ function setupCodex(flow = {}) {
115
+ const check = spawnSync('codex', ['--version'], { stdio: 'ignore' });
116
+ if (check.error || check.status !== 0) {
117
+ throw new Error('Codex CLI was not found on PATH. Install/open Codex first, then run `nado-mcp setup codex` again.');
118
+ }
119
+
120
+ const spec = stdioServerSpec();
121
+ const result = spawnSync('codex', ['mcp', 'add', serverName, '--', spec.command, ...spec.args], {
122
+ stdio: 'inherit',
123
+ });
124
+ if (result.error) throw result.error;
125
+ if (result.status !== 0) throw new Error(`codex mcp add exited with code ${result.status}`);
126
+
127
+ console.log('Registered Nado Language MCP with Codex.');
128
+ printLoginNext(flow);
129
+ }
130
+
131
+ function setupClaudeDesktop(options = {}, flow = {}) {
132
+ const configPath = options.configFile || claudeDesktopConfigPath();
133
+ mkdirSync(path.dirname(configPath), { recursive: true });
134
+
135
+ const config = readJsonConfig(configPath);
136
+
137
+ config.mcpServers = {
138
+ ...(config.mcpServers || {}),
139
+ [serverName]: stdioServerSpec(),
140
+ };
141
+
142
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
143
+ console.log(`Registered Nado Language MCP with Claude Desktop: ${configPath}`);
144
+ if (flow.loginAfter) console.log('Browser login will open next. Restart Claude Desktop after login completes.');
145
+ else console.log('Restart Claude Desktop, then run `nado-mcp login`.');
146
+ }
147
+
148
+ function setupOpenCode(options = {}, flow = {}) {
149
+ const configPath = options.configFile || opencodeConfigPath();
150
+ mkdirSync(path.dirname(configPath), { recursive: true });
151
+
152
+ const config = readJsonConfig(configPath);
153
+ if (!config.$schema) config.$schema = opencodeSchema;
154
+ config.mcp = {
155
+ ...(config.mcp || {}),
156
+ [serverName]: opencodeServerSpec(),
157
+ };
158
+
159
+ writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
160
+ console.log(`Registered Nado Language MCP with OpenCode: ${configPath}`);
161
+ printLoginNext(flow);
162
+ }
163
+
164
+ function setupMcpServersJson(configPath, flow = {}) {
165
+ const resolvedPath = expandHome(configPath);
166
+ mkdirSync(path.dirname(resolvedPath), { recursive: true });
167
+
168
+ const config = readJsonConfig(resolvedPath);
169
+ config.mcpServers = {
170
+ ...(config.mcpServers || {}),
171
+ [serverName]: stdioServerSpec(),
172
+ };
173
+
174
+ writeFileSync(resolvedPath, `${JSON.stringify(config, null, 2)}\n`);
175
+ console.log(`Registered Nado Language MCP in MCP JSON config: ${resolvedPath}`);
176
+ printLoginNext(flow);
177
+ }
178
+
179
+ function doctor() {
180
+ console.log('Nado MCP doctor');
181
+ console.log(`Node: ${process.version}`);
182
+ console.log(`Server: ${serverPath}${existsSync(serverPath) ? '' : ' (missing)'}`);
183
+ console.log(`Auth CLI: ${authPath}${existsSync(authPath) ? '' : ' (missing)'}`);
184
+ console.log(`Auth file: ${defaultUserAuthEnvFile()}${existsSync(defaultUserAuthEnvFile()) ? ' (present)' : ' (missing)'}`);
185
+ console.log(`Claude Desktop config: ${claudeDesktopConfigPath()}`);
186
+ console.log(`OpenCode config: ${opencodeConfigPath()}`);
187
+ }
188
+
189
+ function firstExisting(candidates) {
190
+ return candidates.find((candidate) => existsSync(candidate)) || candidates[0];
191
+ }
192
+
193
+ function splitClientAndSetupOptions(values) {
194
+ const hasClient = values[0] && !values[0].startsWith('-');
195
+ const rawClient = hasClient ? values[0] : '';
196
+ const optionValues = hasClient ? values.slice(1) : values;
197
+ const parsed = takeSetupOptions(optionValues);
198
+ const client = rawClient || (parsed.setupOptions.configFile ? 'mcp-json' : 'generic');
199
+ return { client, setupOptions: parsed.setupOptions, rest: parsed.rest };
200
+ }
201
+
202
+ function takeSetupOptions(values) {
203
+ const setupOptions = {};
204
+ const rest = [];
205
+
206
+ for (let index = 0; index < values.length; index += 1) {
207
+ const value = values[index];
208
+ if (value === '--config-file' || value === '--file') {
209
+ const nextValue = values[index + 1];
210
+ if (!nextValue) throw new Error(`${value} requires a path`);
211
+ setupOptions.configFile = expandHome(nextValue);
212
+ index += 1;
213
+ continue;
214
+ }
215
+ if (value.startsWith('--config-file=')) {
216
+ setupOptions.configFile = expandHome(value.slice('--config-file='.length));
217
+ continue;
218
+ }
219
+ if (value.startsWith('--file=')) {
220
+ setupOptions.configFile = expandHome(value.slice('--file='.length));
221
+ continue;
222
+ }
223
+ rest.push(value);
224
+ }
225
+
226
+ return { setupOptions, rest };
227
+ }
228
+
229
+ function hasOption(values, option) {
230
+ return values.some((value) => value === option || value.startsWith(`${option}=`));
231
+ }
232
+
233
+ function runNode(scriptPath, scriptArgs, options) {
234
+ return new Promise((resolve, reject) => {
235
+ const child = spawn(process.execPath, [scriptPath, ...scriptArgs], options);
236
+ child.on('error', reject);
237
+ child.on('exit', (code) => {
238
+ if (code === 0) resolve();
239
+ else reject(new Error(`${path.basename(scriptPath)} exited with code ${code}`));
240
+ });
241
+ });
242
+ }
243
+
244
+ function stdioServerSpec() {
245
+ return {
246
+ command: process.execPath,
247
+ args: [serverPath],
248
+ };
249
+ }
250
+
251
+ function mcpServersConfig() {
252
+ return {
253
+ mcpServers: {
254
+ [serverName]: stdioServerSpec(),
255
+ },
256
+ };
257
+ }
258
+
259
+ function opencodeServerSpec() {
260
+ const spec = stdioServerSpec();
261
+ return {
262
+ type: 'local',
263
+ command: [spec.command, ...spec.args],
264
+ enabled: true,
265
+ };
266
+ }
267
+
268
+ function opencodeConfig() {
269
+ return {
270
+ $schema: opencodeSchema,
271
+ mcp: {
272
+ [serverName]: opencodeServerSpec(),
273
+ },
274
+ };
275
+ }
276
+
277
+ function printConfig(format) {
278
+ const normalized = String(format || 'all').toLowerCase();
279
+ if (normalized === 'command' || normalized === 'stdio') {
280
+ const spec = stdioServerSpec();
281
+ console.log(`${spec.command} ${spec.args.map(shellQuote).join(' ')}`);
282
+ return;
283
+ }
284
+ if (normalized === 'mcp-json' || normalized === 'claude' || normalized === 'mcpservers') {
285
+ console.log(JSON.stringify(mcpServersConfig(), null, 2));
286
+ return;
287
+ }
288
+ if (normalized === 'opencode' || normalized === 'open-code') {
289
+ console.log(JSON.stringify(opencodeConfig(), null, 2));
290
+ return;
291
+ }
292
+ if (normalized !== 'all' && normalized !== 'generic' && normalized !== 'json') {
293
+ throw new Error('config supports all, command, mcp-json, or opencode');
294
+ }
295
+
296
+ console.log('Stdio command:');
297
+ printIndented(`${stdioServerSpec().command} ${stdioServerSpec().args.map(shellQuote).join(' ')}`);
298
+ console.log('');
299
+ console.log('Generic MCP JSON:');
300
+ printIndented(JSON.stringify(mcpServersConfig(), null, 2));
301
+ console.log('');
302
+ console.log('OpenCode JSON:');
303
+ printIndented(JSON.stringify(opencodeConfig(), null, 2));
304
+ }
305
+
306
+ function printGenericSetup() {
307
+ console.log('Nado Language MCP is a standard stdio MCP server.');
308
+ console.log('Use this config in any MCP-compatible AI client:');
309
+ console.log('');
310
+ printIndented(JSON.stringify(mcpServersConfig(), null, 2));
311
+ console.log('');
312
+ console.log('For OpenCode, use:');
313
+ console.log('');
314
+ printIndented(JSON.stringify(opencodeConfig(), null, 2));
315
+ console.log('');
316
+ console.log('Then run `nado-mcp login`.');
317
+ }
318
+
319
+ function printMcpServersSetup(client) {
320
+ console.log(`No config file path was provided for ${client}.`);
321
+ console.log('Pass `--config-file /path/to/mcp.json`, or paste this into the client MCP config:');
322
+ console.log('');
323
+ printIndented(JSON.stringify(mcpServersConfig(), null, 2));
324
+ }
325
+
326
+ function printUnknownClientSetup(client) {
327
+ console.log(`No built-in config writer for "${client}".`);
328
+ printGenericSetup();
329
+ }
330
+
331
+ function printLoginNext(flow = {}) {
332
+ if (flow.loginAfter) console.log('Browser login will open next.');
333
+ else console.log('Next: run `nado-mcp login`.');
334
+ }
335
+
336
+ function printIndented(text) {
337
+ for (const line of String(text).split('\n')) {
338
+ console.log(` ${line}`);
339
+ }
340
+ }
341
+
342
+ function shellQuote(value) {
343
+ if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) return value;
344
+ return `'${String(value).replaceAll("'", "'\\''")}'`;
345
+ }
346
+
347
+ function readJsonConfig(filePath) {
348
+ if (!existsSync(filePath)) return {};
349
+ const text = readFileSync(filePath, 'utf8').trim();
350
+ if (!text) return {};
351
+
352
+ try {
353
+ return JSON.parse(text);
354
+ } catch {
355
+ try {
356
+ return JSON.parse(removeTrailingJsonCommas(stripJsonComments(text)));
357
+ } catch (error) {
358
+ throw new Error(`Could not parse ${filePath} as JSON/JSONC: ${error instanceof Error ? error.message : String(error)}`);
359
+ }
360
+ }
361
+ }
362
+
363
+ function stripJsonComments(text) {
364
+ let output = '';
365
+ let inString = false;
366
+ let escaped = false;
367
+ let inLineComment = false;
368
+ let inBlockComment = false;
369
+
370
+ for (let index = 0; index < text.length; index += 1) {
371
+ const char = text[index];
372
+ const next = text[index + 1];
373
+
374
+ if (inLineComment) {
375
+ if (char === '\n' || char === '\r') {
376
+ inLineComment = false;
377
+ output += char;
378
+ }
379
+ continue;
380
+ }
381
+
382
+ if (inBlockComment) {
383
+ if (char === '*' && next === '/') {
384
+ inBlockComment = false;
385
+ index += 1;
386
+ } else if (char === '\n' || char === '\r') {
387
+ output += char;
388
+ }
389
+ continue;
390
+ }
391
+
392
+ if (!inString && char === '/' && next === '/') {
393
+ inLineComment = true;
394
+ index += 1;
395
+ continue;
396
+ }
397
+
398
+ if (!inString && char === '/' && next === '*') {
399
+ inBlockComment = true;
400
+ index += 1;
401
+ continue;
402
+ }
403
+
404
+ output += char;
405
+
406
+ if (inString) {
407
+ if (escaped) {
408
+ escaped = false;
409
+ } else if (char === '\\') {
410
+ escaped = true;
411
+ } else if (char === '"') {
412
+ inString = false;
413
+ }
414
+ } else if (char === '"') {
415
+ inString = true;
416
+ }
417
+ }
418
+
419
+ return output;
420
+ }
421
+
422
+ function removeTrailingJsonCommas(text) {
423
+ let output = '';
424
+ let inString = false;
425
+ let escaped = false;
426
+
427
+ for (let index = 0; index < text.length; index += 1) {
428
+ const char = text[index];
429
+
430
+ if (inString) {
431
+ output += char;
432
+ if (escaped) {
433
+ escaped = false;
434
+ } else if (char === '\\') {
435
+ escaped = true;
436
+ } else if (char === '"') {
437
+ inString = false;
438
+ }
439
+ continue;
440
+ }
441
+
442
+ if (char === '"') {
443
+ inString = true;
444
+ output += char;
445
+ continue;
446
+ }
447
+
448
+ if (char === ',') {
449
+ let lookahead = index + 1;
450
+ while (/\s/.test(text[lookahead] || '')) lookahead += 1;
451
+ if (text[lookahead] === '}' || text[lookahead] === ']') continue;
452
+ }
453
+
454
+ output += char;
455
+ }
456
+
457
+ return output;
458
+ }
459
+
460
+ function defaultUserAuthEnvFile() {
461
+ if (process.platform === 'win32') {
462
+ return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Nado', 'MCP', 'auth.env');
463
+ }
464
+ if (process.platform === 'darwin') {
465
+ return path.join(os.homedir(), 'Library', 'Application Support', 'Nado', 'MCP', 'auth.env');
466
+ }
467
+ return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'nado', 'mcp', 'auth.env');
468
+ }
469
+
470
+ function claudeDesktopConfigPath() {
471
+ if (process.platform === 'win32') {
472
+ return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
473
+ }
474
+ if (process.platform === 'darwin') {
475
+ return path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
476
+ }
477
+ return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'Claude', 'claude_desktop_config.json');
478
+ }
479
+
480
+ function opencodeConfigPath() {
481
+ if (process.env.OPENCODE_CONFIG) return expandHome(process.env.OPENCODE_CONFIG);
482
+ return path.join(os.homedir(), '.config', 'opencode', 'opencode.json');
483
+ }
484
+
485
+ function expandHome(value) {
486
+ const text = String(value || '');
487
+ if (text === '~') return os.homedir();
488
+ if (text.startsWith('~/') || text.startsWith('~\\')) return path.join(os.homedir(), text.slice(2));
489
+ return text;
490
+ }
491
+
492
+ function printHelp() {
493
+ console.log(`Nado Language MCP
494
+
495
+ Usage:
496
+ nado-mcp connect codex Register in Codex, then log in
497
+ nado-mcp connect claude Register in Claude Desktop, then log in
498
+ nado-mcp connect opencode Register in OpenCode, then log in
499
+ nado-mcp setup <client> Register without login
500
+ nado-mcp setup mcp-json --file PATH Merge into a generic mcpServers JSON file
501
+ nado-mcp config Print generic MCP config snippets
502
+ nado-mcp login Open browser login and save local auth
503
+ nado-mcp status Show local auth status
504
+ nado-mcp logout Remove local auth tokens
505
+ nado-mcp server Start the stdio MCP server
506
+ nado-mcp probe list List exposed MCP tools
507
+ nado-mcp doctor Print local paths and auth status
508
+
509
+ Supported automatic setup clients:
510
+ codex, claude, opencode
511
+
512
+ Universal fallback:
513
+ Any MCP-compatible AI client can run the stdio server command printed by:
514
+ nado-mcp config
515
+
516
+ AI-agent friendly flow:
517
+ 1. Install the package
518
+ 2. Run: nado-mcp connect <client>
519
+ 3. If the client is unknown, paste the JSON from nado-mcp config
520
+ `);
521
+ }