@nado-language/mcp 0.1.4 → 0.1.6

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
@@ -42,25 +42,25 @@ nado-mcp login
42
42
  Source checkout alias:
43
43
 
44
44
  ```bash
45
- npm run mcp:nado:auth -- --provider google
45
+ npm run mcp:nado:auth
46
46
  ```
47
47
 
48
- This opens the provider login page, relays the Supabase OAuth callback through the existing Azure Static Web Apps site, receives the final callback on `127.0.0.1`, and writes ignored local tokens to the user's OS config directory for package installs or `.env.mcp.local` in a repo checkout:
48
+ This opens the Nado web connect page. The user signs in there with any login method already supported by Nado, then the page sends the browser session to the local `127.0.0.1` helper and writes ignored local tokens to the user's OS config directory for package installs or `.env.mcp.local` in a repo checkout:
49
49
 
50
50
  ```bash
51
51
  NADO_MCP_ACCESS_TOKEN='supabase-user-access-token'
52
52
  NADO_MCP_REFRESH_TOKEN='supabase-user-refresh-token'
53
53
  ```
54
54
 
55
- By default this uses the existing Azure Static Web Apps production site as a static OAuth relay. It does not require a new Azure Function, App Service, database, or paid runtime. The local helper still receives the final callback on `127.0.0.1`; Azure only serves the static relay page.
55
+ By default this uses the existing Azure Static Web Apps production site as a provider-neutral connect page. It does not require a new Azure Function, App Service, database, or paid runtime. The browser posts the session directly to the local helper; tokens are not placed in the browser URL.
56
56
 
57
- The installed CLI opens the Nado relay page first. The relay stores the local callback in browser session storage, then sends Supabase a fixed redirect URL. This avoids Supabase rejecting a dynamic `redirect_to` URL with `local_callback` query parameters and falling back to the normal Nado web site.
57
+ Legacy direct OAuth remains available with `nado-mcp login --provider google|kakao|apple`, but normal users should omit `--provider`.
58
58
 
59
59
  The MCP server refreshes expired access tokens with `NADO_MCP_REFRESH_TOKEN` and updates the auth file when Supabase rotates the refresh token.
60
60
 
61
- Supported local browser providers are `google`, `kakao`, and `apple`. Naver login is not available in the local MCP flow yet because the current Naver Edge Function uses fixed web/native redirect URLs.
61
+ Supported web login methods are the same as Nado web login, including Google, Kakao, Naver, and Apple.
62
62
 
63
- Supabase Auth must allow the Azure relay redirect URL:
63
+ Legacy direct OAuth provider mode requires Supabase Auth to allow the Azure relay redirect URL:
64
64
 
65
65
  ```text
66
66
  https://language.nado.ai.kr/auth/mcp-callback
@@ -77,6 +77,7 @@ http://127.0.0.1:*/callback
77
77
  You can inspect or clear local MCP auth with:
78
78
 
79
79
  ```bash
80
+ nado-mcp --version
80
81
  nado-mcp status
81
82
  nado-mcp logout
82
83
  ```
@@ -88,6 +89,14 @@ npm run mcp:nado:auth -- status
88
89
  npm run mcp:nado:auth -- logout
89
90
  ```
90
91
 
92
+ If the browser shows an error about an old, incomplete, truncated, or invalid PKCE login URL, close every old Nado MCP login tab, upgrade the package, and rerun login. Printed login URLs are single-run URLs and should not be reused:
93
+
94
+ ```bash
95
+ npm install --global @nado-language/mcp@latest
96
+ nado-mcp --version
97
+ nado-mcp login
98
+ ```
99
+
91
100
  Manual access-token option:
92
101
 
93
102
  ```bash
@@ -119,6 +128,7 @@ Optional environment:
119
128
  ```bash
120
129
  export NADO_MCP_SUPABASE_URL='https://ptbwzhxifxdnfmqsiugi.supabase.co'
121
130
  export NADO_MCP_SUPABASE_ANON_KEY='...'
131
+ export NADO_MCP_CONNECT_URL='https://language.nado.ai.kr/mcp/connect'
122
132
  export NADO_MCP_AUTH_RELAY_URL='https://language.nado.ai.kr/auth/mcp-callback'
123
133
  ```
124
134
 
@@ -257,7 +257,7 @@ async function getAccessToken() {
257
257
  const email = clampText(env.NADO_MCP_EMAIL || '', 320);
258
258
  const password = env.NADO_MCP_PASSWORD || '';
259
259
  if (!email || !password) {
260
- throw new Error('AUTH_NOT_CONFIGURED: run `nado-mcp login`, or in a repo checkout run `npm run mcp:nado:auth -- --provider google`, before using Nado MCP tools.');
260
+ throw new Error('AUTH_NOT_CONFIGURED: run `nado-mcp login`, or in a repo checkout run `npm run mcp:nado:auth`, before using Nado MCP tools.');
261
261
  }
262
262
 
263
263
  if (cachedPasswordGrant && cachedPasswordGrant.expiresAt > Date.now() + 60_000) {
@@ -11,17 +11,21 @@ import { fileURLToPath } from 'node:url';
11
11
  const DEFAULT_SUPABASE_URL = 'https://ptbwzhxifxdnfmqsiugi.supabase.co';
12
12
  const DEFAULT_SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InB0Ynd6aHhpZnhkbmZtcXNpdWdpIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzU1MTU4MjEsImV4cCI6MjA5MTA5MTgyMX0.c0SU8lvIb8BbwhYyI529dn7tQUfwTl1cGqeahGKaD_g';
13
13
  const DEFAULT_RELAY_URL = 'https://language.nado.ai.kr/auth/mcp-callback';
14
+ const DEFAULT_CONNECT_URL = 'https://language.nado.ai.kr/mcp/connect';
14
15
  const SUPPORTED_PROVIDERS = new Set(['google', 'kakao', 'apple']);
15
16
  const SUPPORTED_REDIRECT_MODES = new Set(['azure', 'local']);
16
17
 
17
18
  const scriptDir = path.dirname(fileURLToPath(import.meta.url));
18
19
  const repoRoot = path.resolve(scriptDir, '..');
20
+ const packageVersion = readPackageVersion();
19
21
 
20
22
  const { command, options } = parseCli(process.argv.slice(2));
21
23
 
22
24
  try {
23
25
  if (options.help || command === 'help') {
24
26
  printHelp();
27
+ } else if (command === 'version') {
28
+ console.log(packageVersion);
25
29
  } else if (command === 'login') {
26
30
  await login(options);
27
31
  } else if (command === 'status') {
@@ -39,19 +43,23 @@ try {
39
43
  function parseCli(argv) {
40
44
  let command = 'login';
41
45
  let args = argv;
42
- if (argv[0] && !argv[0].startsWith('-')) {
46
+ if (argv[0] === '--version' || argv[0] === '-v') {
47
+ command = 'version';
48
+ args = argv.slice(1);
49
+ } else if (argv[0] && !argv[0].startsWith('-')) {
43
50
  command = argv[0];
44
51
  args = argv.slice(1);
45
52
  }
46
53
 
47
54
  const options = {
48
- provider: 'google',
55
+ provider: '',
49
56
  port: 0,
50
57
  envFile: process.env.NADO_MCP_AUTH_ENV_FILE || defaultAuthEnvFile(),
51
58
  supabaseUrl: process.env.NADO_MCP_SUPABASE_URL || process.env.EXPO_PUBLIC_SUPABASE_URL || DEFAULT_SUPABASE_URL,
52
59
  anonKey: process.env.NADO_MCP_SUPABASE_ANON_KEY || process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY || DEFAULT_SUPABASE_ANON_KEY,
53
60
  redirectMode: 'azure',
54
61
  relayUrl: process.env.NADO_MCP_AUTH_RELAY_URL || DEFAULT_RELAY_URL,
62
+ connectUrl: process.env.NADO_MCP_CONNECT_URL || DEFAULT_CONNECT_URL,
55
63
  noOpen: false,
56
64
  timeoutMs: 300_000,
57
65
  help: false,
@@ -74,9 +82,11 @@ function parseCli(argv) {
74
82
  else if (flag === '--anon-key') options.anonKey = readValue();
75
83
  else if (flag === '--redirect-mode') options.redirectMode = readValue();
76
84
  else if (flag === '--relay-url') options.relayUrl = readValue();
85
+ else if (flag === '--connect-url') options.connectUrl = readValue();
77
86
  else if (flag === '--timeout-ms') options.timeoutMs = Number(readValue());
78
87
  else if (flag === '--no-open') options.noOpen = true;
79
88
  else if (flag === '--help' || flag === '-h') options.help = true;
89
+ else if (flag === '--version' || flag === '-v') command = 'version';
80
90
  else throw new Error(`Unknown option: ${arg}`);
81
91
  }
82
92
 
@@ -116,10 +126,7 @@ function defaultUserAuthEnvFile() {
116
126
 
117
127
  async function login(options) {
118
128
  const provider = String(options.provider || '').toLowerCase();
119
- if (provider === 'naver') {
120
- throw new Error('Naver login is not available for local MCP auth yet because the Naver Edge Function uses fixed redirect URLs. Use google, kakao, or apple.');
121
- }
122
- if (!SUPPORTED_PROVIDERS.has(provider)) {
129
+ if (provider && !SUPPORTED_PROVIDERS.has(provider)) {
123
130
  throw new Error(`Unsupported provider: ${provider}. Use google, kakao, or apple.`);
124
131
  }
125
132
 
@@ -138,6 +145,10 @@ async function login(options) {
138
145
  sendHtml(response, 404, 'Nado MCP Auth', 'Unknown callback path.');
139
146
  return;
140
147
  }
148
+ if (request.method === 'OPTIONS') {
149
+ sendCors(response, 204);
150
+ return;
151
+ }
141
152
  if (settled) {
142
153
  sendHtml(response, 200, 'Nado MCP Auth', 'Login already completed. You can close this tab.');
143
154
  return;
@@ -147,6 +158,23 @@ async function login(options) {
147
158
  if (oauthError) throw new Error(oauthError);
148
159
  if (url.searchParams.get('state') !== state) throw new Error('Invalid OAuth state.');
149
160
 
161
+ if (request.method === 'POST') {
162
+ const session = await readPostedSession(request);
163
+ const user = await fetchUser(options.supabaseUrl, options.anonKey, session.access_token);
164
+
165
+ writeAuthEnv(options.envFile, {
166
+ NADO_MCP_SUPABASE_URL: options.supabaseUrl,
167
+ NADO_MCP_SUPABASE_ANON_KEY: options.anonKey,
168
+ NADO_MCP_ACCESS_TOKEN: session.access_token,
169
+ NADO_MCP_REFRESH_TOKEN: session.refresh_token,
170
+ });
171
+
172
+ settled = true;
173
+ sendJson(response, 200, { ok: true, email: user.email || null });
174
+ resolve({ session, user });
175
+ return;
176
+ }
177
+
150
178
  const code = url.searchParams.get('code');
151
179
  if (!code) throw new Error('Missing OAuth code.');
152
180
 
@@ -190,9 +218,9 @@ async function login(options) {
190
218
  codeChallenge,
191
219
  });
192
220
 
193
- console.log(`Opening browser for Nado MCP login (${provider}).`);
221
+ console.log('Opening browser for Nado MCP login.');
194
222
  console.log(`Local callback: ${localCallbackUrl.toString()}`);
195
- if (options.redirectMode === 'azure') console.log(`Azure relay: ${options.relayUrl}`);
223
+ if (provider && options.redirectMode === 'azure') console.log(`Azure relay: ${options.relayUrl}`);
196
224
  if (!options.noOpen) openBrowser(browserUrl);
197
225
  console.log(`If the browser did not open, visit:\n${browserUrl}`);
198
226
 
@@ -214,14 +242,21 @@ async function login(options) {
214
242
  function loginTimeoutError(options) {
215
243
  return new Error([
216
244
  'Timed out waiting for browser login.',
217
- `Rerun \`nado-mcp login --provider ${options.provider} --timeout-ms 900000\` and keep the terminal open until the browser says login completed.`,
245
+ 'Rerun `nado-mcp login --timeout-ms 900000` and keep the terminal open until the browser says login completed.',
218
246
  'If the browser did not open, copy the printed URL into the same desktop browser where you can sign in.',
219
- 'If Google login succeeds but the browser lands on the normal Nado site, upgrade @nado-language/mcp and confirm Supabase Auth allows the exact relay URL without query parameters.',
220
- 'If it still times out after the relay page says it is returning to the local helper, check that the browser can reach the printed 127.0.0.1 local callback URL.',
247
+ 'If login succeeds but this still times out, check that the browser can reach the printed 127.0.0.1 local callback URL.',
221
248
  ].join(' '));
222
249
  }
223
250
 
224
251
  function buildBrowserLoginUrl({ options, provider, localCallbackUrl, codeChallenge }) {
252
+ if (!provider) {
253
+ return buildConnectUrl({
254
+ connectUrl: options.connectUrl,
255
+ localCallbackUrl,
256
+ supabaseUrl: options.supabaseUrl,
257
+ });
258
+ }
259
+
225
260
  if (options.redirectMode === 'local') {
226
261
  return buildAuthorizeUrl({
227
262
  supabaseUrl: options.supabaseUrl,
@@ -249,9 +284,21 @@ function buildRelayStartUrl({ relayUrl: value, localCallbackUrl, provider, supab
249
284
  relayUrl.searchParams.set('provider', provider);
250
285
  relayUrl.searchParams.set('supabase_url', supabaseUrl);
251
286
  relayUrl.searchParams.set('code_challenge', codeChallenge);
287
+ relayUrl.searchParams.set('client_version', packageVersion);
252
288
  return relayUrl.toString();
253
289
  }
254
290
 
291
+ function buildConnectUrl({ connectUrl: value, localCallbackUrl, supabaseUrl }) {
292
+ const connectUrl = new URL(value);
293
+ if (connectUrl.protocol !== 'https:' && connectUrl.hostname !== 'localhost' && connectUrl.hostname !== '127.0.0.1') {
294
+ throw new Error('--connect-url must be an HTTPS URL unless it points to localhost.');
295
+ }
296
+ connectUrl.searchParams.set('local_callback', localCallbackUrl);
297
+ connectUrl.searchParams.set('supabase_url', supabaseUrl);
298
+ connectUrl.searchParams.set('client_version', packageVersion);
299
+ return connectUrl.toString();
300
+ }
301
+
255
302
  function printStatus(options) {
256
303
  const values = readEnvFile(options.envFile);
257
304
  const accessToken = process.env.NADO_MCP_ACCESS_TOKEN || values.NADO_MCP_ACCESS_TOKEN || process.env.NADO_ACCESS_TOKEN || '';
@@ -331,6 +378,34 @@ async function readJsonResponse(response) {
331
378
  }
332
379
  }
333
380
 
381
+ async function readPostedSession(request) {
382
+ const body = await readRequestJson(request);
383
+ const session = body?.session && typeof body.session === 'object' ? body.session : body;
384
+ const accessToken = typeof session?.access_token === 'string' ? session.access_token : '';
385
+ const refreshToken = typeof session?.refresh_token === 'string' ? session.refresh_token : '';
386
+ if (!accessToken || !refreshToken) {
387
+ throw new Error('Missing session tokens from Nado web login.');
388
+ }
389
+ return {
390
+ access_token: accessToken,
391
+ refresh_token: refreshToken,
392
+ };
393
+ }
394
+
395
+ async function readRequestJson(request) {
396
+ const chunks = [];
397
+ for await (const chunk of request) {
398
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
399
+ }
400
+ const text = Buffer.concat(chunks).toString('utf8');
401
+ if (!text) return {};
402
+ try {
403
+ return JSON.parse(text);
404
+ } catch {
405
+ throw new Error('Invalid JSON callback payload.');
406
+ }
407
+ }
408
+
334
409
  function openBrowser(url) {
335
410
  const platform = os.platform();
336
411
  const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
@@ -456,6 +531,24 @@ function resolvePath(value) {
456
531
  return path.isAbsolute(value) ? value : path.resolve(process.cwd(), value);
457
532
  }
458
533
 
534
+ function readPackageVersion() {
535
+ const candidates = [
536
+ path.join(repoRoot, 'packages', 'nado-mcp', 'package.json'),
537
+ path.join(scriptDir, '..', 'package.json'),
538
+ ];
539
+ for (const filePath of candidates) {
540
+ try {
541
+ const pkg = JSON.parse(readFileSync(filePath, 'utf8'));
542
+ if (pkg?.name === '@nado-language/mcp' && typeof pkg.version === 'string') {
543
+ return pkg.version;
544
+ }
545
+ } catch {
546
+ // Continue to the next candidate.
547
+ }
548
+ }
549
+ return '0.0.0-dev';
550
+ }
551
+
459
552
  function listen(server, port) {
460
553
  return new Promise((resolve, reject) => {
461
554
  server.once('error', reject);
@@ -477,10 +570,29 @@ function closeServer(server) {
477
570
  }
478
571
 
479
572
  function sendHtml(response, status, title, message) {
480
- response.writeHead(status, { 'content-type': 'text/html; charset=utf-8' });
573
+ response.writeHead(status, { ...callbackCorsHeaders(), 'content-type': 'text/html; charset=utf-8' });
481
574
  response.end(`<!doctype html><html><head><meta charset="utf-8"><title>${escapeHtml(title)}</title></head><body><h1>${escapeHtml(title)}</h1><p>${escapeHtml(message)}</p></body></html>`);
482
575
  }
483
576
 
577
+ function sendJson(response, status, body) {
578
+ response.writeHead(status, { ...callbackCorsHeaders(), 'content-type': 'application/json; charset=utf-8' });
579
+ response.end(JSON.stringify(body));
580
+ }
581
+
582
+ function sendCors(response, status) {
583
+ response.writeHead(status, callbackCorsHeaders());
584
+ response.end();
585
+ }
586
+
587
+ function callbackCorsHeaders() {
588
+ return {
589
+ 'access-control-allow-origin': '*',
590
+ 'access-control-allow-methods': 'GET,POST,OPTIONS',
591
+ 'access-control-allow-headers': 'content-type',
592
+ 'access-control-allow-private-network': 'true',
593
+ };
594
+ }
595
+
484
596
  function escapeHtml(value) {
485
597
  return String(value)
486
598
  .replace(/&/g, '&amp;')
@@ -494,28 +606,36 @@ function printHelp() {
494
606
  console.log(`Nado Language MCP browser auth
495
607
 
496
608
  Usage:
497
- nado-mcp login [--provider google|kakao|apple]
609
+ nado-mcp login
610
+ nado-mcp login --provider google|kakao|apple
498
611
  nado-mcp status
499
612
  nado-mcp logout
613
+ nado-mcp-auth --version
500
614
 
501
615
  Repo checkout aliases:
502
- npm run mcp:nado:auth -- [login] [--provider google|kakao|apple]
616
+ npm run mcp:nado:auth -- [login]
503
617
  npm run mcp:nado:auth -- status
504
618
  npm run mcp:nado:auth -- logout
505
619
 
506
620
  Options:
507
- --provider <name> OAuth provider for login. Default: google
621
+ --provider <name> Legacy direct OAuth provider. Normally omit this and sign in on the Nado web page.
508
622
  --auth-file <path> Auth env file to write. Default: OS user config in package installs, .env.mcp.local in repo checkouts
509
623
  --port <number> Local callback port. Default: 0 (random)
510
624
  --redirect-mode <mode> azure or local. Default: azure
511
625
  --relay-url <url> Azure Static Web Apps relay URL. Default: ${DEFAULT_RELAY_URL}
626
+ --connect-url <url> Provider-neutral Nado web connect URL. Default: ${DEFAULT_CONNECT_URL}
512
627
  --no-open Print the URL without opening a browser
513
628
  --timeout-ms <number> Login wait timeout. Default: 300000
514
629
  --supabase-url <url> Supabase project URL
515
630
  --anon-key <key> Supabase anon key
631
+ --version Print installed Nado MCP version
632
+
633
+ Default mode opens the Nado web connect page. Sign in there with any provider
634
+ supported by the web app, then the page posts the browser session to the local
635
+ 127.0.0.1 helper. Tokens are not placed in the browser URL.
516
636
 
517
- Default mode uses the existing Azure Static Web Apps site as a zero-new-resource
518
- OAuth relay. Supabase Auth must allow this redirect URL:
637
+ Legacy provider mode uses the existing Azure Static Web Apps site as a
638
+ zero-new-resource OAuth relay. Supabase Auth must allow this redirect URL:
519
639
  ${DEFAULT_RELAY_URL}
520
640
 
521
641
  The relay starts login with local state in browser sessionStorage, then sends
@@ -11,6 +11,7 @@ const sourceRoot = path.resolve(scriptDir, '..');
11
11
  const packageDistDir = scriptDir;
12
12
  const serverName = 'nado-language';
13
13
  const opencodeSchema = 'https://opencode.ai/config.json';
14
+ const packageVersion = readPackageVersion();
14
15
 
15
16
  const serverPath = firstExisting([
16
17
  path.join(packageDistDir, 'nado-language-server.mjs'),
@@ -31,6 +32,8 @@ const args = process.argv.slice(3);
31
32
  try {
32
33
  if (command === 'help' || command === '--help' || command === '-h') {
33
34
  printHelp();
35
+ } else if (command === 'version' || command === '--version' || command === '-v') {
36
+ console.log(packageVersion);
34
37
  } else if (command === 'server') {
35
38
  await runNode(serverPath, args, { stdio: 'inherit' });
36
39
  } else if (command === 'login') {
@@ -240,6 +243,7 @@ async function doctor() {
240
243
  const probe = probeTools();
241
244
 
242
245
  console.log('Nado MCP doctor');
246
+ console.log(`Version: ${packageVersion}`);
243
247
  console.log(`Node: ${process.version}`);
244
248
  console.log(`Server: ${serverPath}${existsSync(serverPath) ? '' : ' (missing)'}`);
245
249
  console.log(`Auth CLI: ${authPath}${existsSync(authPath) ? '' : ' (missing)'}`);
@@ -789,6 +793,24 @@ function expandHome(value) {
789
793
  return text;
790
794
  }
791
795
 
796
+ function readPackageVersion() {
797
+ const candidates = [
798
+ path.join(sourceRoot, 'packages', 'nado-mcp', 'package.json'),
799
+ path.join(packageDistDir, '..', 'package.json'),
800
+ ];
801
+ for (const filePath of candidates) {
802
+ try {
803
+ const pkg = JSON.parse(readFileSync(filePath, 'utf8'));
804
+ if (pkg?.name === '@nado-language/mcp' && typeof pkg.version === 'string') {
805
+ return pkg.version;
806
+ }
807
+ } catch {
808
+ // Continue to the next candidate.
809
+ }
810
+ }
811
+ return '0.0.0-dev';
812
+ }
813
+
792
814
  function printHelp() {
793
815
  console.log(`Nado Language MCP
794
816
 
@@ -807,6 +829,7 @@ Usage:
807
829
  nado-mcp server Start the stdio MCP server
808
830
  nado-mcp probe list List exposed MCP tools
809
831
  nado-mcp doctor Print local paths and auth status
832
+ nado-mcp --version Print installed Nado MCP version
810
833
 
811
834
  Supported automatic setup clients:
812
835
  codex, claude, opencode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nado-language/mcp",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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,