@nado-language/mcp 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,12 +6,19 @@ This stdio MCP server lets AI chat clients save and practice Nado Language study
6
6
 
7
7
  - `nado_whoami`: validates the configured Nado account.
8
8
  - `nado_save_flashcard`: saves a structured flashcard generated by the user's chat AI. Nado does not call AI for this path.
9
+ - `nado_save_study_item`: alias of `nado_save_flashcard` for natural memorization requests such as "암기할래", "외울래", "암기장에 넣어줘", and "단어장에 추가".
9
10
  - `nado_analyze_and_save_flashcard`: Pro/Admin only. Nado AI generates the learner definition, usage note, examples, and variants, then saves the flashcard.
10
11
  - `nado_list_study_items`: loads saved flashcards for the authenticated user.
11
12
  - `nado_generate_practice`: builds practice exercises from saved flashcards only.
12
13
 
13
14
  The default MCP path is designed to avoid double charging for AI. ChatGPT, Claude, Codex, or another user-paid AI model should fill the structured flashcard fields, and Nado only validates/saves them. Nado AI quality generation is a separate Pro/Admin tool.
14
15
 
16
+ Intent routing:
17
+
18
+ - If the user provides a new English item and says "암기할래", "외울래", "암기장에 추가", "단어장에 넣어줘", "remember this", or "add this to my flashcards", use `nado_save_flashcard` or `nado_save_study_item`.
19
+ - If the user asks to study already saved cards with phrases like "외울래", "암기 연습", "퀴즈 내줘", "쓰기연습", "영작하기", or "복습할래", use `nado_generate_practice`.
20
+ - If the user wants to inspect saved items first, use `nado_list_study_items`.
21
+
15
22
  ## Authentication
16
23
 
17
24
  Install from npm:
@@ -115,6 +122,18 @@ export NADO_MCP_AUTH_RELAY_URL='https://language.nado.ai.kr/auth/mcp-callback'
115
122
 
116
123
  Installed package, one-command setup plus browser login:
117
124
 
125
+ ```bash
126
+ nado-mcp connect
127
+ ```
128
+
129
+ To force every supported local config writer:
130
+
131
+ ```bash
132
+ nado-mcp connect all
133
+ ```
134
+
135
+ Client-specific setup remains available:
136
+
118
137
  ```bash
119
138
  nado-mcp connect codex
120
139
  nado-mcp connect claude
@@ -127,6 +146,9 @@ For Codex, the command uses the Codex CLI when it is on `PATH`. If the user only
127
146
  - Windows: `%USERPROFILE%\.codex\config.toml`
128
147
 
129
148
  Restart Codex Desktop after login completes.
149
+ If a client was already open, start a new chat/session after restart. Most local MCP clients load tool definitions when the session starts, so a config that was just written may not appear inside an already-running conversation.
150
+
151
+ ChatGPT is different: it uses hosted/remote MCP apps configured from ChatGPT Apps settings, not local stdio config files. The local package can prepare local clients such as Codex, Claude Desktop, OpenCode, and generic JSON-based MCP clients.
130
152
 
131
153
  Setup without login:
132
154
 
@@ -166,6 +188,7 @@ When using the installed package, `nado-mcp login` saves auth to the user's OS c
166
188
  Basic tool discovery:
167
189
 
168
190
  ```bash
191
+ nado-mcp doctor
169
192
  nado-mcp probe list
170
193
  ```
171
194
 
@@ -231,9 +254,13 @@ npm run mcp:nado:probe -- save-nado-ai "serendipity"
231
254
  Install/register/login flow:
232
255
 
233
256
  ```bash
234
- nado-mcp connect codex
235
- nado-mcp connect claude
236
- nado-mcp connect opencode
257
+ nado-mcp connect
258
+ ```
259
+
260
+ Force all supported local config writers:
261
+
262
+ ```bash
263
+ nado-mcp connect all
237
264
  ```
238
265
 
239
266
  Generic MCP fallback:
@@ -242,3 +269,10 @@ Generic MCP fallback:
242
269
  nado-mcp config
243
270
  nado-mcp login
244
271
  ```
272
+
273
+ Diagnostics:
274
+
275
+ ```bash
276
+ nado-mcp doctor
277
+ nado-mcp probe list
278
+ ```
@@ -30,6 +30,40 @@ let cachedRefreshGrant = null;
30
30
  const FLASHCARD_TYPES = new Set(['word', 'phrase', 'sentence_structure']);
31
31
  const PRACTICE_MODES = new Set(['writing', 'vocabulary_quiz', 'conversation', 'sentence_completion', 'translation']);
32
32
 
33
+ const saveFlashcardInputSchema = {
34
+ type: 'object',
35
+ required: ['original', 'definition'],
36
+ properties: {
37
+ original: {
38
+ type: 'string',
39
+ minLength: 1,
40
+ description: 'English word, phrase, or sentence to save. Example: for "hello 암기할래", use "hello".',
41
+ },
42
+ type: { type: 'string', enum: ['word', 'phrase', 'sentence_structure'], default: 'phrase' },
43
+ definition: { type: 'string', minLength: 1, description: 'Learner-language definition or translation generated by the chat AI.' },
44
+ inlineDefinition: { type: 'string', description: 'Short review-side meaning. Defaults to a compact definition.' },
45
+ explanation: { type: 'string', description: 'Optional nuance, usage note, or grammar explanation.' },
46
+ exampleSentences: {
47
+ type: 'array',
48
+ items: { type: 'string' },
49
+ description: 'Optional examples generated by the user chat AI.',
50
+ },
51
+ variants: {
52
+ type: 'array',
53
+ items: { type: 'string' },
54
+ description: 'Optional related forms, collocations, or variants.',
55
+ },
56
+ contextSentence: { type: 'string', description: 'Original sentence/context from the chat. Defaults to text.' },
57
+ articleTitle: { type: 'string', description: 'Source title shown in analysis. Defaults to MCP Chat.' },
58
+ sourceLang: { type: 'string', default: 'ko', description: 'User language for definitions.' },
59
+ targetLang: { type: 'string', default: 'en', description: 'Language being learned.' },
60
+ userLevel: { type: 'string', default: 'B1', description: 'CEFR level used by Nado analysis.' },
61
+ articleId: { type: 'string', default: 'mcp-chat' },
62
+ sourceHighlightId: { type: 'string', default: 'mcp-chat' },
63
+ },
64
+ additionalProperties: false,
65
+ };
66
+
33
67
  const tools = [
34
68
  {
35
69
  name: 'nado_whoami',
@@ -42,40 +76,17 @@ const tools = [
42
76
  },
43
77
  {
44
78
  name: 'nado_save_flashcard',
45
- description: 'Save a structured flashcard that the chat model already generated. This does not call Nado AI; the user/chat AI is responsible for content quality.',
46
- inputSchema: {
47
- type: 'object',
48
- required: ['original', 'definition'],
49
- properties: {
50
- original: { type: 'string', minLength: 1, description: 'English word, phrase, or sentence to save.' },
51
- type: { type: 'string', enum: ['word', 'phrase', 'sentence_structure'], default: 'phrase' },
52
- definition: { type: 'string', minLength: 1, description: 'Learner-language definition or translation.' },
53
- inlineDefinition: { type: 'string', description: 'Short review-side meaning. Defaults to a compact definition.' },
54
- explanation: { type: 'string', description: 'Optional nuance, usage note, or grammar explanation.' },
55
- exampleSentences: {
56
- type: 'array',
57
- items: { type: 'string' },
58
- description: 'Optional examples generated by the user chat AI.',
59
- },
60
- variants: {
61
- type: 'array',
62
- items: { type: 'string' },
63
- description: 'Optional related forms, collocations, or variants.',
64
- },
65
- contextSentence: { type: 'string', description: 'Original sentence/context from the chat. Defaults to text.' },
66
- articleTitle: { type: 'string', description: 'Source title shown in analysis. Defaults to MCP Chat.' },
67
- sourceLang: { type: 'string', default: 'ko', description: 'User language for definitions.' },
68
- targetLang: { type: 'string', default: 'en', description: 'Language being learned.' },
69
- userLevel: { type: 'string', default: 'B1', description: 'CEFR level used by Nado analysis.' },
70
- articleId: { type: 'string', default: 'mcp-chat' },
71
- sourceHighlightId: { type: 'string', default: 'mcp-chat' },
72
- },
73
- additionalProperties: false,
74
- },
79
+ description: 'Save a structured Nado Language flashcard that the chat model already generated. Use this when the user gives an English item and asks to memorize/save it, for example "암기장에 추가해줘", "암기할래", "외울래", "단어장에 넣어줘", "remember this", or "add this to my flashcards". This does not call Nado AI; the user/chat AI supplies the content quality.',
80
+ inputSchema: saveFlashcardInputSchema,
81
+ },
82
+ {
83
+ name: 'nado_save_study_item',
84
+ description: 'Alias of nado_save_flashcard for study-list and memorization intents. Prefer this or nado_save_flashcard when the user asks to save a specific word/phrase/sentence to Nado for later memorization, including Korean requests like "암기할래", "외울래", "암기장에 넣어줘", or "단어장에 추가". This free path does not call Nado AI.',
85
+ inputSchema: saveFlashcardInputSchema,
75
86
  },
76
87
  {
77
88
  name: 'nado_analyze_and_save_flashcard',
78
- description: 'Pro/Admin only: use Nado AI to produce validated definitions/examples for a word, phrase, or sentence, then save the flashcard.',
89
+ description: 'Pro/Admin only: use Nado AI to produce validated definitions/examples for a word, phrase, or sentence, then save the flashcard. Use only when the user explicitly wants Nado-verified/pro quality analysis or examples; normal memorize/save requests should use nado_save_flashcard so the user AI supplies the content.',
79
90
  inputSchema: {
80
91
  type: 'object',
81
92
  required: ['original'],
@@ -94,7 +105,7 @@ const tools = [
94
105
  },
95
106
  {
96
107
  name: 'nado_list_study_items',
97
- description: 'Load saved Nado Language vocabulary and sentence cards for practice.',
108
+ description: 'Load saved Nado Language vocabulary and sentence cards. Use when the user asks what is saved, wants to inspect the memorization list, or asks to review existing Nado study items before practice.',
98
109
  inputSchema: {
99
110
  type: 'object',
100
111
  properties: {
@@ -107,7 +118,7 @@ const tools = [
107
118
  },
108
119
  {
109
120
  name: 'nado_generate_practice',
110
- description: 'Generate English practice content using only the authenticated user saved Nado Language study cards.',
121
+ description: 'Generate English practice content using only the authenticated user saved Nado Language study cards. Use when the user wants to study/review already saved items: "외울래", "암기 연습", "퀴즈 내줘", "쓰기연습", "영작하기", "복습할래", "practice my saved words". If the user supplies a new English item to save, use nado_save_flashcard instead.',
111
122
  inputSchema: {
112
123
  type: 'object',
113
124
  required: ['mode'],
@@ -883,8 +894,8 @@ function stableHash(value) {
883
894
  async function callTool(name, args = {}) {
884
895
  if (name === 'nado_whoami') return handleWhoami();
885
896
  if (name === 'nado_save_flashcard') return handleSaveFlashcard(args);
897
+ if (name === 'nado_save_study_item') return handleSaveFlashcard(args);
886
898
  if (name === 'nado_analyze_and_save_flashcard') return handleAnalyzeAndSaveFlashcard(args);
887
- if (name === 'nado_save_study_item') return handleAnalyzeAndSaveFlashcard({ ...args, original: args.original || args.text });
888
899
  if (name === 'nado_list_study_items') return handleListStudyItems(args);
889
900
  if (name === 'nado_generate_practice') return handleGeneratePractice(args);
890
901
  throw new Error(`UNKNOWN_TOOL: ${name}`);
@@ -199,7 +199,7 @@ async function login(options) {
199
199
 
200
200
  let timeoutId;
201
201
  const timeout = new Promise((_, reject) => {
202
- timeoutId = setTimeout(() => reject(new Error('Timed out waiting for browser login.')), options.timeoutMs);
202
+ timeoutId = setTimeout(() => reject(loginTimeoutError(options)), options.timeoutMs);
203
203
  });
204
204
 
205
205
  try {
@@ -212,6 +212,15 @@ async function login(options) {
212
212
  }
213
213
  }
214
214
 
215
+ function loginTimeoutError(options) {
216
+ return new Error([
217
+ 'Timed out waiting for browser login.',
218
+ `Rerun \`nado-mcp login --provider ${options.provider} --timeout-ms 900000\` and keep the terminal open until the browser says login completed.`,
219
+ 'If the browser did not open, copy the printed URL into the same desktop browser where you can sign in.',
220
+ 'If you already signed in but it still timed out, check that the browser can reach the printed 127.0.0.1 local callback URL and that Supabase Auth allows the relay URL.',
221
+ ].join(' '));
222
+ }
223
+
215
224
  function buildRedirectTo(options, localCallbackUrl) {
216
225
  if (options.redirectMode === 'local') return localCallbackUrl;
217
226
 
@@ -45,15 +45,19 @@ try {
45
45
  await runNode(probePath, args, { stdio: 'inherit' });
46
46
  } else if (command === 'setup') {
47
47
  const parsed = splitClientAndSetupOptions(args);
48
- await setup(parsed.client, parsed.setupOptions);
48
+ const didRegister = await setup(parsed.client, parsed.setupOptions);
49
+ if (didRegister) printToolProbeSummary();
49
50
  } else if (command === 'connect' || command === 'install') {
50
51
  const parsed = splitClientAndSetupOptions(args);
51
- await setup(parsed.client, parsed.setupOptions, { loginAfter: true });
52
- await runAuth(['login', ...parsed.rest]);
52
+ const didRegister = await setup(parsed.client, parsed.setupOptions, { loginAfter: true });
53
+ if (didRegister) {
54
+ printToolProbeSummary();
55
+ await runAuth(['login', ...parsed.rest]);
56
+ }
53
57
  } else if (command === 'config') {
54
58
  printConfig(args[0] || 'all');
55
59
  } else if (command === 'doctor') {
56
- doctor();
60
+ await doctor();
57
61
  } else {
58
62
  throw new Error(`Unknown command: ${command}`);
59
63
  }
@@ -72,6 +76,16 @@ async function runAuth(authArgs) {
72
76
 
73
77
  async function setup(client, options = {}, flow = {}) {
74
78
  const normalized = String(client || '').toLowerCase();
79
+ if (normalized === 'auto') {
80
+ return setupAutoLocalClients(options, flow);
81
+ }
82
+ if (normalized === 'all') {
83
+ return setupAllLocalClients(options, flow);
84
+ }
85
+ if (normalized === 'chatgpt' || normalized === 'openai' || normalized === 'chatgpt-app') {
86
+ printChatGptSetup();
87
+ return false;
88
+ }
75
89
  if (normalized === 'codex') {
76
90
  setupCodex(flow);
77
91
  return true;
@@ -111,6 +125,33 @@ async function setup(client, options = {}, flow = {}) {
111
125
  return false;
112
126
  }
113
127
 
128
+ function setupAutoLocalClients(options = {}, flow = {}) {
129
+ const targets = [];
130
+ if (isCodexAvailable()) targets.push(['codex', () => setupCodex(flow)]);
131
+ if (isClaudeDesktopAvailable(options)) targets.push(['claude', () => setupClaudeDesktop(options, flow)]);
132
+ if (isOpenCodeAvailable(options)) targets.push(['opencode', () => setupOpenCode(options, flow)]);
133
+
134
+ if (targets.length === 0) {
135
+ console.log('No supported local MCP client was detected.');
136
+ printGenericSetup();
137
+ return false;
138
+ }
139
+
140
+ console.log(`Detected local MCP client${targets.length === 1 ? '' : 's'}: ${targets.map(([name]) => name).join(', ')}`);
141
+ for (const [, register] of targets) register();
142
+ printChatGptLocalLimit();
143
+ return true;
144
+ }
145
+
146
+ function setupAllLocalClients(options = {}, flow = {}) {
147
+ console.log('Registering Nado Language MCP with every supported local config writer.');
148
+ setupCodexDesktopConfig(flow);
149
+ setupClaudeDesktop(options, flow);
150
+ setupOpenCode(options, flow);
151
+ printChatGptLocalLimit();
152
+ return true;
153
+ }
154
+
114
155
  function setupCodex(flow = {}) {
115
156
  const check = spawnSync('codex', ['--version'], { stdio: 'ignore' });
116
157
  if (check.error || check.status !== 0) {
@@ -191,15 +232,182 @@ function setupMcpServersJson(configPath, flow = {}) {
191
232
  printLoginNext(flow);
192
233
  }
193
234
 
194
- function doctor() {
235
+ async function doctor() {
236
+ const auth = authStatus(configuredAuthEnvFile());
237
+ const codex = codexRegistrationStatus();
238
+ const claude = jsonRegistrationStatus(claudeDesktopConfigPath(), ['mcpServers', serverName]);
239
+ const opencode = jsonRegistrationStatus(opencodeConfigPath(), ['mcp', serverName]);
240
+ const probe = probeTools();
241
+
195
242
  console.log('Nado MCP doctor');
196
243
  console.log(`Node: ${process.version}`);
197
244
  console.log(`Server: ${serverPath}${existsSync(serverPath) ? '' : ' (missing)'}`);
198
245
  console.log(`Auth CLI: ${authPath}${existsSync(authPath) ? '' : ' (missing)'}`);
199
- console.log(`Auth file: ${defaultUserAuthEnvFile()}${existsSync(defaultUserAuthEnvFile()) ? ' (present)' : ' (missing)'}`);
200
- console.log(`Codex Desktop config: ${codexDesktopConfigPath()}`);
201
- console.log(`Claude Desktop config: ${claudeDesktopConfigPath()}`);
202
- console.log(`OpenCode config: ${opencodeConfigPath()}`);
246
+ console.log(`Local MCP server check: ${probe.ok ? `ok (${probe.tools.join(', ')})` : `failed (${probe.error})`}`);
247
+ console.log(`Auth file: ${auth.filePath}${auth.fileExists ? ' (present)' : ' (missing)'}`);
248
+ console.log(`Auth access token: ${auth.accessToken ? `present${tokenExpiryText(auth.accessToken)}` : 'missing'}`);
249
+ console.log(`Auth refresh token: ${auth.refreshToken ? 'present' : 'missing'}`);
250
+ console.log(`Auth email/password fallback: ${auth.email ? `configured for ${auth.email}` : 'missing'}`);
251
+ console.log(`Codex Desktop config: ${registrationText(codex)}`);
252
+ console.log(`Claude Desktop config: ${registrationText(claude)}`);
253
+ console.log(`OpenCode config: ${registrationText(opencode)}`);
254
+ console.log('');
255
+ console.log('If the server check is ok but Nado tools are not visible in the AI app:');
256
+ console.log(' 1. Fully quit and restart the desktop app after `nado-mcp connect`.');
257
+ console.log(' 2. Start a new chat/session; existing sessions may not reload newly added MCP tools.');
258
+ console.log(' 3. Run `nado-mcp status` for auth and `nado-mcp probe list` for local server tools.');
259
+ }
260
+
261
+ function printToolProbeSummary() {
262
+ const probe = probeTools();
263
+ if (probe.ok) {
264
+ console.log(`Local MCP server check: ok (${probe.tools.join(', ')})`);
265
+ } else {
266
+ console.log(`Local MCP server check failed: ${probe.error}`);
267
+ }
268
+ }
269
+
270
+ function probeTools() {
271
+ if (!existsSync(probePath)) return { ok: false, error: `probe script missing at ${probePath}` };
272
+
273
+ const result = spawnSync(process.execPath, [probePath, 'list'], {
274
+ encoding: 'utf8',
275
+ env: {
276
+ ...process.env,
277
+ NADO_MCP_SKIP_DOTENV: '1',
278
+ },
279
+ });
280
+
281
+ if (result.error) return { ok: false, error: result.error.message };
282
+ if (result.status !== 0) {
283
+ const message = (result.stderr || result.stdout || `probe exited with code ${result.status}`).trim();
284
+ return { ok: false, error: message };
285
+ }
286
+
287
+ try {
288
+ const parsed = JSON.parse(result.stdout);
289
+ const tools = (parsed.tools || []).map((tool) => tool.name).filter(Boolean);
290
+ if (tools.length === 0) return { ok: false, error: 'no tools returned by server' };
291
+ return { ok: true, tools };
292
+ } catch (error) {
293
+ return { ok: false, error: `could not parse probe output: ${error instanceof Error ? error.message : String(error)}` };
294
+ }
295
+ }
296
+
297
+ function configuredAuthEnvFile() {
298
+ return process.env.NADO_MCP_AUTH_ENV_FILE ? expandHome(process.env.NADO_MCP_AUTH_ENV_FILE) : defaultUserAuthEnvFile();
299
+ }
300
+
301
+ function authStatus(filePath) {
302
+ const values = readEnvFile(filePath);
303
+ const accessToken = process.env.NADO_MCP_ACCESS_TOKEN || values.NADO_MCP_ACCESS_TOKEN || process.env.NADO_ACCESS_TOKEN || '';
304
+ const refreshToken = process.env.NADO_MCP_REFRESH_TOKEN || values.NADO_MCP_REFRESH_TOKEN || process.env.NADO_REFRESH_TOKEN || '';
305
+ const email = process.env.NADO_MCP_EMAIL || values.NADO_MCP_EMAIL || '';
306
+
307
+ return {
308
+ filePath,
309
+ fileExists: existsSync(filePath),
310
+ accessToken,
311
+ refreshToken,
312
+ email,
313
+ };
314
+ }
315
+
316
+ function codexRegistrationStatus() {
317
+ const configPath = codexDesktopConfigPath();
318
+ if (!existsSync(configPath)) return { path: configPath, fileExists: false, registered: false };
319
+
320
+ try {
321
+ const text = readFileSync(configPath, 'utf8');
322
+ const sectionPattern = new RegExp(`^\\s*\\[mcp_servers\\.${escapeRegExp(serverName)}]\\s*$`, 'm');
323
+ return { path: configPath, fileExists: true, registered: sectionPattern.test(text) };
324
+ } catch (error) {
325
+ return { path: configPath, fileExists: true, registered: false, error: error instanceof Error ? error.message : String(error) };
326
+ }
327
+ }
328
+
329
+ function jsonRegistrationStatus(configPath, keyPath) {
330
+ if (!existsSync(configPath)) return { path: configPath, fileExists: false, registered: false };
331
+
332
+ try {
333
+ const config = readJsonConfig(configPath);
334
+ return {
335
+ path: configPath,
336
+ fileExists: true,
337
+ registered: getPathValue(config, keyPath) !== undefined,
338
+ };
339
+ } catch (error) {
340
+ return { path: configPath, fileExists: true, registered: false, error: error instanceof Error ? error.message : String(error) };
341
+ }
342
+ }
343
+
344
+ function registrationText(status) {
345
+ if (status.error) return `${status.path} (error: ${status.error})`;
346
+ if (!status.fileExists) return `${status.path} (missing)`;
347
+ return `${status.path} (${status.registered ? 'registered' : 'not registered'})`;
348
+ }
349
+
350
+ function getPathValue(value, keys) {
351
+ let current = value;
352
+ for (const key of keys) {
353
+ if (!current || typeof current !== 'object' || !(key in current)) return undefined;
354
+ current = current[key];
355
+ }
356
+ return current;
357
+ }
358
+
359
+ function readEnvFile(filePath) {
360
+ if (!existsSync(filePath)) return {};
361
+ const values = {};
362
+ for (const line of readFileSync(filePath, 'utf8').split(/\r?\n/)) {
363
+ const parsed = parseEnvLine(line);
364
+ if (parsed) values[parsed.key] = parsed.value;
365
+ }
366
+ return values;
367
+ }
368
+
369
+ function parseEnvLine(line) {
370
+ const trimmed = line.trim();
371
+ if (!trimmed || trimmed.startsWith('#')) return null;
372
+
373
+ const withoutExport = trimmed.startsWith('export ') ? trimmed.slice('export '.length).trimStart() : trimmed;
374
+ const separator = withoutExport.indexOf('=');
375
+ if (separator <= 0) return null;
376
+
377
+ const key = withoutExport.slice(0, separator).trim();
378
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) return null;
379
+
380
+ let value = withoutExport.slice(separator + 1).trim();
381
+ const quote = value[0];
382
+ if ((quote === '"' || quote === "'") && value.endsWith(quote)) {
383
+ value = value.slice(1, -1);
384
+ } else {
385
+ value = value.replace(/\s+#.*$/, '').trim();
386
+ }
387
+
388
+ return { key, value };
389
+ }
390
+
391
+ function tokenExpiryText(token) {
392
+ const expiresAt = jwtExpiresAtMs(token);
393
+ if (!expiresAt) return '';
394
+ return `, expires ${new Date(expiresAt).toISOString()}`;
395
+ }
396
+
397
+ function jwtExpiresAtMs(token) {
398
+ const parts = String(token || '').split('.');
399
+ if (parts.length < 2) return null;
400
+ try {
401
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
402
+ const exp = Number(payload.exp);
403
+ return Number.isFinite(exp) ? exp * 1000 : null;
404
+ } catch {
405
+ return null;
406
+ }
407
+ }
408
+
409
+ function escapeRegExp(value) {
410
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
203
411
  }
204
412
 
205
413
  function firstExisting(candidates) {
@@ -211,7 +419,7 @@ function splitClientAndSetupOptions(values) {
211
419
  const rawClient = hasClient ? values[0] : '';
212
420
  const optionValues = hasClient ? values.slice(1) : values;
213
421
  const parsed = takeSetupOptions(optionValues);
214
- const client = rawClient || (parsed.setupOptions.configFile ? 'mcp-json' : 'generic');
422
+ const client = rawClient || (parsed.setupOptions.configFile ? 'mcp-json' : 'auto');
215
423
  return { client, setupOptions: parsed.setupOptions, rest: parsed.rest };
216
424
  }
217
425
 
@@ -290,6 +498,36 @@ function opencodeConfig() {
290
498
  };
291
499
  }
292
500
 
501
+ function isCodexAvailable() {
502
+ if (process.env.NADO_MCP_CODEX_CONFIG_FILE) return true;
503
+ if (commandExists('codex')) return true;
504
+ return existsSync(path.dirname(codexDesktopConfigPath()));
505
+ }
506
+
507
+ function isClaudeDesktopAvailable(options = {}) {
508
+ if (options.configFile) return true;
509
+ if (existsSync(claudeDesktopConfigPath())) return true;
510
+ if (process.platform === 'darwin') {
511
+ return existsSync('/Applications/Claude.app') || existsSync(path.join(os.homedir(), 'Applications', 'Claude.app'));
512
+ }
513
+ if (process.platform === 'win32') {
514
+ const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
515
+ return existsSync(path.join(localAppData, 'Programs', 'Claude', 'Claude.exe'));
516
+ }
517
+ return commandExists('claude');
518
+ }
519
+
520
+ function isOpenCodeAvailable(options = {}) {
521
+ if (options.configFile || process.env.OPENCODE_CONFIG) return true;
522
+ if (existsSync(opencodeConfigPath())) return true;
523
+ return commandExists('opencode');
524
+ }
525
+
526
+ function commandExists(commandName) {
527
+ const result = spawnSync(commandName, ['--version'], { stdio: 'ignore' });
528
+ return !result.error && result.status === 0;
529
+ }
530
+
293
531
  function codexDesktopTomlSection() {
294
532
  const spec = stdioServerSpec();
295
533
  return [
@@ -363,6 +601,16 @@ function printGenericSetup() {
363
601
  console.log('Then run `nado-mcp login`.');
364
602
  }
365
603
 
604
+ function printChatGptSetup() {
605
+ console.log('ChatGPT does not use local stdio MCP config files.');
606
+ console.log('Use a hosted/remote MCP endpoint in ChatGPT Apps settings, then scan tools in ChatGPT.');
607
+ console.log('Nado local package setup can configure Codex, Claude Desktop, OpenCode, and generic local MCP JSON clients.');
608
+ }
609
+
610
+ function printChatGptLocalLimit() {
611
+ console.log('Note: ChatGPT requires a hosted/remote MCP app registration in ChatGPT settings; local package setup cannot install into ChatGPT directly.');
612
+ }
613
+
366
614
  function printMcpServersSetup(client) {
367
615
  console.log(`No config file path was provided for ${client}.`);
368
616
  console.log('Pass `--config-file /path/to/mcp.json`, or paste this into the client MCP config:');
@@ -545,6 +793,8 @@ function printHelp() {
545
793
  console.log(`Nado Language MCP
546
794
 
547
795
  Usage:
796
+ nado-mcp connect Auto-register detected local MCP clients, then log in
797
+ nado-mcp connect all Register all supported local config writers, then log in
548
798
  nado-mcp connect codex Register in Codex CLI/Desktop, then log in
549
799
  nado-mcp connect claude Register in Claude Desktop, then log in
550
800
  nado-mcp connect opencode Register in OpenCode, then log in
@@ -567,7 +817,7 @@ Universal fallback:
567
817
 
568
818
  AI-agent friendly flow:
569
819
  1. Install the package
570
- 2. Run: nado-mcp connect <client>
571
- 3. If the client is unknown, paste the JSON from nado-mcp config
820
+ 2. Run: nado-mcp connect
821
+ 3. If the client is unknown or remote-only, paste the JSON from nado-mcp config
572
822
  `);
573
823
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nado-language/mcp",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Nado Language MCP server for saving AI-generated English flashcards and practicing saved materials.",
5
5
  "type": "module",
6
6
  "private": false,