@nado-language/mcp 0.1.5 → 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
@@ -94,7 +94,7 @@ If the browser shows an error about an old, incomplete, truncated, or invalid PK
94
94
  ```bash
95
95
  npm install --global @nado-language/mcp@latest
96
96
  nado-mcp --version
97
- nado-mcp login --provider google
97
+ nado-mcp login
98
98
  ```
99
99
 
100
100
  Manual access-token option:
@@ -128,6 +128,7 @@ Optional environment:
128
128
  ```bash
129
129
  export NADO_MCP_SUPABASE_URL='https://ptbwzhxifxdnfmqsiugi.supabase.co'
130
130
  export NADO_MCP_SUPABASE_ANON_KEY='...'
131
+ export NADO_MCP_CONNECT_URL='https://language.nado.ai.kr/mcp/connect'
131
132
  export NADO_MCP_AUTH_RELAY_URL='https://language.nado.ai.kr/auth/mcp-callback'
132
133
  ```
133
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,6 +11,7 @@ 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
 
@@ -51,13 +52,14 @@ function parseCli(argv) {
51
52
  }
52
53
 
53
54
  const options = {
54
- provider: 'google',
55
+ provider: '',
55
56
  port: 0,
56
57
  envFile: process.env.NADO_MCP_AUTH_ENV_FILE || defaultAuthEnvFile(),
57
58
  supabaseUrl: process.env.NADO_MCP_SUPABASE_URL || process.env.EXPO_PUBLIC_SUPABASE_URL || DEFAULT_SUPABASE_URL,
58
59
  anonKey: process.env.NADO_MCP_SUPABASE_ANON_KEY || process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY || DEFAULT_SUPABASE_ANON_KEY,
59
60
  redirectMode: 'azure',
60
61
  relayUrl: process.env.NADO_MCP_AUTH_RELAY_URL || DEFAULT_RELAY_URL,
62
+ connectUrl: process.env.NADO_MCP_CONNECT_URL || DEFAULT_CONNECT_URL,
61
63
  noOpen: false,
62
64
  timeoutMs: 300_000,
63
65
  help: false,
@@ -80,6 +82,7 @@ function parseCli(argv) {
80
82
  else if (flag === '--anon-key') options.anonKey = readValue();
81
83
  else if (flag === '--redirect-mode') options.redirectMode = readValue();
82
84
  else if (flag === '--relay-url') options.relayUrl = readValue();
85
+ else if (flag === '--connect-url') options.connectUrl = readValue();
83
86
  else if (flag === '--timeout-ms') options.timeoutMs = Number(readValue());
84
87
  else if (flag === '--no-open') options.noOpen = true;
85
88
  else if (flag === '--help' || flag === '-h') options.help = true;
@@ -123,10 +126,7 @@ function defaultUserAuthEnvFile() {
123
126
 
124
127
  async function login(options) {
125
128
  const provider = String(options.provider || '').toLowerCase();
126
- if (provider === 'naver') {
127
- 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.');
128
- }
129
- if (!SUPPORTED_PROVIDERS.has(provider)) {
129
+ if (provider && !SUPPORTED_PROVIDERS.has(provider)) {
130
130
  throw new Error(`Unsupported provider: ${provider}. Use google, kakao, or apple.`);
131
131
  }
132
132
 
@@ -145,6 +145,10 @@ async function login(options) {
145
145
  sendHtml(response, 404, 'Nado MCP Auth', 'Unknown callback path.');
146
146
  return;
147
147
  }
148
+ if (request.method === 'OPTIONS') {
149
+ sendCors(response, 204);
150
+ return;
151
+ }
148
152
  if (settled) {
149
153
  sendHtml(response, 200, 'Nado MCP Auth', 'Login already completed. You can close this tab.');
150
154
  return;
@@ -154,6 +158,23 @@ async function login(options) {
154
158
  if (oauthError) throw new Error(oauthError);
155
159
  if (url.searchParams.get('state') !== state) throw new Error('Invalid OAuth state.');
156
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
+
157
178
  const code = url.searchParams.get('code');
158
179
  if (!code) throw new Error('Missing OAuth code.');
159
180
 
@@ -197,9 +218,9 @@ async function login(options) {
197
218
  codeChallenge,
198
219
  });
199
220
 
200
- console.log(`Opening browser for Nado MCP login (${provider}).`);
221
+ console.log('Opening browser for Nado MCP login.');
201
222
  console.log(`Local callback: ${localCallbackUrl.toString()}`);
202
- if (options.redirectMode === 'azure') console.log(`Azure relay: ${options.relayUrl}`);
223
+ if (provider && options.redirectMode === 'azure') console.log(`Azure relay: ${options.relayUrl}`);
203
224
  if (!options.noOpen) openBrowser(browserUrl);
204
225
  console.log(`If the browser did not open, visit:\n${browserUrl}`);
205
226
 
@@ -221,14 +242,21 @@ async function login(options) {
221
242
  function loginTimeoutError(options) {
222
243
  return new Error([
223
244
  'Timed out waiting for browser login.',
224
- `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.',
225
246
  'If the browser did not open, copy the printed URL into the same desktop browser where you can sign in.',
226
- '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.',
227
- '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.',
228
248
  ].join(' '));
229
249
  }
230
250
 
231
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
+
232
260
  if (options.redirectMode === 'local') {
233
261
  return buildAuthorizeUrl({
234
262
  supabaseUrl: options.supabaseUrl,
@@ -260,6 +288,17 @@ function buildRelayStartUrl({ relayUrl: value, localCallbackUrl, provider, supab
260
288
  return relayUrl.toString();
261
289
  }
262
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
+
263
302
  function printStatus(options) {
264
303
  const values = readEnvFile(options.envFile);
265
304
  const accessToken = process.env.NADO_MCP_ACCESS_TOKEN || values.NADO_MCP_ACCESS_TOKEN || process.env.NADO_ACCESS_TOKEN || '';
@@ -339,6 +378,34 @@ async function readJsonResponse(response) {
339
378
  }
340
379
  }
341
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
+
342
409
  function openBrowser(url) {
343
410
  const platform = os.platform();
344
411
  const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'cmd' : 'xdg-open';
@@ -503,10 +570,29 @@ function closeServer(server) {
503
570
  }
504
571
 
505
572
  function sendHtml(response, status, title, message) {
506
- response.writeHead(status, { 'content-type': 'text/html; charset=utf-8' });
573
+ response.writeHead(status, { ...callbackCorsHeaders(), 'content-type': 'text/html; charset=utf-8' });
507
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>`);
508
575
  }
509
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
+
510
596
  function escapeHtml(value) {
511
597
  return String(value)
512
598
  .replace(/&/g, '&amp;')
@@ -520,30 +606,36 @@ function printHelp() {
520
606
  console.log(`Nado Language MCP browser auth
521
607
 
522
608
  Usage:
523
- nado-mcp login [--provider google|kakao|apple]
609
+ nado-mcp login
610
+ nado-mcp login --provider google|kakao|apple
524
611
  nado-mcp status
525
612
  nado-mcp logout
526
613
  nado-mcp-auth --version
527
614
 
528
615
  Repo checkout aliases:
529
- npm run mcp:nado:auth -- [login] [--provider google|kakao|apple]
616
+ npm run mcp:nado:auth -- [login]
530
617
  npm run mcp:nado:auth -- status
531
618
  npm run mcp:nado:auth -- logout
532
619
 
533
620
  Options:
534
- --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.
535
622
  --auth-file <path> Auth env file to write. Default: OS user config in package installs, .env.mcp.local in repo checkouts
536
623
  --port <number> Local callback port. Default: 0 (random)
537
624
  --redirect-mode <mode> azure or local. Default: azure
538
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}
539
627
  --no-open Print the URL without opening a browser
540
628
  --timeout-ms <number> Login wait timeout. Default: 300000
541
629
  --supabase-url <url> Supabase project URL
542
630
  --anon-key <key> Supabase anon key
543
631
  --version Print installed Nado MCP version
544
632
 
545
- Default mode uses the existing Azure Static Web Apps site as a zero-new-resource
546
- OAuth relay. Supabase Auth must allow this redirect URL:
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.
636
+
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:
547
639
  ${DEFAULT_RELAY_URL}
548
640
 
549
641
  The relay starts login with local state in browser sessionStorage, then sends
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nado-language/mcp",
3
- "version": "0.1.5",
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,