@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 +37 -3
- package/dist/nado-language-server.mjs +45 -34
- package/dist/nado-mcp-auth.mjs +10 -1
- package/dist/nado-mcp-cli.mjs +262 -12
- 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:
|
|
@@ -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
|
|
235
|
-
|
|
236
|
-
|
|
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
|
|
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
|
@@ -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(
|
|
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
|
|
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
|
-
await setup(parsed.client, parsed.setupOptions, { loginAfter: true });
|
|
52
|
-
|
|
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(`
|
|
200
|
-
console.log(`
|
|
201
|
-
console.log(`
|
|
202
|
-
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, '\\$&');
|
|
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' : '
|
|
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
|
|
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
|
}
|