@nado-language/mcp 0.1.2 → 0.1.4
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 +27 -3
- package/dist/nado-language-server.mjs +45 -34
- package/dist/nado-mcp-auth.mjs +43 -10
- package/dist/nado-mcp-cli.mjs +179 -8
- package/package.json +1 -1
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:
|
|
@@ -47,6 +54,8 @@ NADO_MCP_REFRESH_TOKEN='supabase-user-refresh-token'
|
|
|
47
54
|
|
|
48
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.
|
|
49
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.
|
|
58
|
+
|
|
50
59
|
The MCP server refreshes expired access tokens with `NADO_MCP_REFRESH_TOKEN` and updates the auth file when Supabase rotates the refresh token.
|
|
51
60
|
|
|
52
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.
|
|
@@ -57,6 +66,8 @@ Supabase Auth must allow the Azure relay redirect URL:
|
|
|
57
66
|
https://language.nado.ai.kr/auth/mcp-callback
|
|
58
67
|
```
|
|
59
68
|
|
|
69
|
+
Register the exact URL above. Do not include `local_callback`, `provider`, or other query parameters in the Supabase allow list.
|
|
70
|
+
|
|
60
71
|
For direct local callback mode, run with `--redirect-mode local` and allow:
|
|
61
72
|
|
|
62
73
|
```text
|
|
@@ -139,6 +150,7 @@ For Codex, the command uses the Codex CLI when it is on `PATH`. If the user only
|
|
|
139
150
|
- Windows: `%USERPROFILE%\.codex\config.toml`
|
|
140
151
|
|
|
141
152
|
Restart Codex Desktop after login completes.
|
|
153
|
+
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.
|
|
142
154
|
|
|
143
155
|
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.
|
|
144
156
|
|
|
@@ -180,6 +192,7 @@ When using the installed package, `nado-mcp login` saves auth to the user's OS c
|
|
|
180
192
|
Basic tool discovery:
|
|
181
193
|
|
|
182
194
|
```bash
|
|
195
|
+
nado-mcp doctor
|
|
183
196
|
nado-mcp probe list
|
|
184
197
|
```
|
|
185
198
|
|
|
@@ -245,9 +258,13 @@ npm run mcp:nado:probe -- save-nado-ai "serendipity"
|
|
|
245
258
|
Install/register/login flow:
|
|
246
259
|
|
|
247
260
|
```bash
|
|
248
|
-
nado-mcp connect
|
|
249
|
-
|
|
250
|
-
|
|
261
|
+
nado-mcp connect
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Force all supported local config writers:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
nado-mcp connect all
|
|
251
268
|
```
|
|
252
269
|
|
|
253
270
|
Generic MCP fallback:
|
|
@@ -256,3 +273,10 @@ Generic MCP fallback:
|
|
|
256
273
|
nado-mcp config
|
|
257
274
|
nado-mcp login
|
|
258
275
|
```
|
|
276
|
+
|
|
277
|
+
Diagnostics:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
nado-mcp doctor
|
|
281
|
+
nado-mcp probe list
|
|
282
|
+
```
|
|
@@ -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
|
|
46
|
-
inputSchema:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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}`);
|
package/dist/nado-mcp-auth.mjs
CHANGED
|
@@ -183,23 +183,22 @@ async function login(options) {
|
|
|
183
183
|
|
|
184
184
|
const localCallbackUrl = new URL(`http://127.0.0.1:${address.port}/callback`);
|
|
185
185
|
localCallbackUrl.searchParams.set('state', state);
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
supabaseUrl: options.supabaseUrl,
|
|
186
|
+
const browserUrl = buildBrowserLoginUrl({
|
|
187
|
+
options,
|
|
189
188
|
provider,
|
|
190
|
-
|
|
189
|
+
localCallbackUrl: localCallbackUrl.toString(),
|
|
191
190
|
codeChallenge,
|
|
192
191
|
});
|
|
193
192
|
|
|
194
193
|
console.log(`Opening browser for Nado MCP login (${provider}).`);
|
|
195
194
|
console.log(`Local callback: ${localCallbackUrl.toString()}`);
|
|
196
195
|
if (options.redirectMode === 'azure') console.log(`Azure relay: ${options.relayUrl}`);
|
|
197
|
-
if (!options.noOpen) openBrowser(
|
|
198
|
-
console.log(`If the browser did not open, visit:\n${
|
|
196
|
+
if (!options.noOpen) openBrowser(browserUrl);
|
|
197
|
+
console.log(`If the browser did not open, visit:\n${browserUrl}`);
|
|
199
198
|
|
|
200
199
|
let timeoutId;
|
|
201
200
|
const timeout = new Promise((_, reject) => {
|
|
202
|
-
timeoutId = setTimeout(() => reject(
|
|
201
|
+
timeoutId = setTimeout(() => reject(loginTimeoutError(options)), options.timeoutMs);
|
|
203
202
|
});
|
|
204
203
|
|
|
205
204
|
try {
|
|
@@ -212,14 +211,44 @@ async function login(options) {
|
|
|
212
211
|
}
|
|
213
212
|
}
|
|
214
213
|
|
|
215
|
-
function
|
|
216
|
-
|
|
214
|
+
function loginTimeoutError(options) {
|
|
215
|
+
return new Error([
|
|
216
|
+
'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.`,
|
|
218
|
+
'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.',
|
|
221
|
+
].join(' '));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function buildBrowserLoginUrl({ options, provider, localCallbackUrl, codeChallenge }) {
|
|
225
|
+
if (options.redirectMode === 'local') {
|
|
226
|
+
return buildAuthorizeUrl({
|
|
227
|
+
supabaseUrl: options.supabaseUrl,
|
|
228
|
+
provider,
|
|
229
|
+
redirectTo: localCallbackUrl,
|
|
230
|
+
codeChallenge,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return buildRelayStartUrl({
|
|
235
|
+
relayUrl: options.relayUrl,
|
|
236
|
+
localCallbackUrl,
|
|
237
|
+
provider,
|
|
238
|
+
supabaseUrl: options.supabaseUrl,
|
|
239
|
+
codeChallenge,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
217
242
|
|
|
218
|
-
|
|
243
|
+
function buildRelayStartUrl({ relayUrl: value, localCallbackUrl, provider, supabaseUrl, codeChallenge }) {
|
|
244
|
+
const relayUrl = new URL(value);
|
|
219
245
|
if (relayUrl.protocol !== 'https:') {
|
|
220
246
|
throw new Error('--relay-url must be an HTTPS URL when --redirect-mode azure is used.');
|
|
221
247
|
}
|
|
222
248
|
relayUrl.searchParams.set('local_callback', localCallbackUrl);
|
|
249
|
+
relayUrl.searchParams.set('provider', provider);
|
|
250
|
+
relayUrl.searchParams.set('supabase_url', supabaseUrl);
|
|
251
|
+
relayUrl.searchParams.set('code_challenge', codeChallenge);
|
|
223
252
|
return relayUrl.toString();
|
|
224
253
|
}
|
|
225
254
|
|
|
@@ -489,6 +518,10 @@ Default mode uses the existing Azure Static Web Apps site as a zero-new-resource
|
|
|
489
518
|
OAuth relay. Supabase Auth must allow this redirect URL:
|
|
490
519
|
${DEFAULT_RELAY_URL}
|
|
491
520
|
|
|
521
|
+
The relay starts login with local state in browser sessionStorage, then sends
|
|
522
|
+
Supabase the fixed redirect URL above. Do not add local_callback query strings
|
|
523
|
+
to the Supabase allow list.
|
|
524
|
+
|
|
492
525
|
The optional local mode requires Supabase Auth to allow:
|
|
493
526
|
http://127.0.0.1:*/callback
|
|
494
527
|
`);
|
package/dist/nado-mcp-cli.mjs
CHANGED
|
@@ -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
52
|
const didRegister = await setup(parsed.client, parsed.setupOptions, { loginAfter: true });
|
|
52
|
-
if (didRegister)
|
|
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
|
}
|
|
@@ -228,15 +232,182 @@ function setupMcpServersJson(configPath, flow = {}) {
|
|
|
228
232
|
printLoginNext(flow);
|
|
229
233
|
}
|
|
230
234
|
|
|
231
|
-
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
|
+
|
|
232
242
|
console.log('Nado MCP doctor');
|
|
233
243
|
console.log(`Node: ${process.version}`);
|
|
234
244
|
console.log(`Server: ${serverPath}${existsSync(serverPath) ? '' : ' (missing)'}`);
|
|
235
245
|
console.log(`Auth CLI: ${authPath}${existsSync(authPath) ? '' : ' (missing)'}`);
|
|
236
|
-
console.log(`
|
|
237
|
-
console.log(`
|
|
238
|
-
console.log(`
|
|
239
|
-
console.log(`
|
|
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, '\\$&');
|
|
240
411
|
}
|
|
241
412
|
|
|
242
413
|
function firstExisting(candidates) {
|