@nado-language/mcp 0.1.6 → 0.1.8

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
@@ -7,6 +7,7 @@ This stdio MCP server lets AI chat clients save and practice Nado Language study
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
9
  - `nado_save_study_item`: alias of `nado_save_flashcard` for natural memorization requests such as "암기할래", "외울래", "암기장에 넣어줘", and "단어장에 추가".
10
+ - `nado_update_study_item`: updates an existing saved Nado card with a revised meaning, description/explanation, examples, variants, or context. Use it for requests like "hello 설명 추가", "description 추가", "뜻 수정", and "예문 추가".
10
11
  - `nado_analyze_and_save_flashcard`: Pro/Admin only. Nado AI generates the learner definition, usage note, examples, and variants, then saves the flashcard.
11
12
  - `nado_list_study_items`: loads saved flashcards for the authenticated user.
12
13
  - `nado_generate_practice`: builds practice exercises from saved flashcards only.
@@ -15,7 +16,8 @@ The default MCP path is designed to avoid double charging for AI. ChatGPT, Claud
15
16
 
16
17
  Intent routing:
17
18
 
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 provides a new English item and says "나두 암기장", "nado 암기장", "암기할래", "외울래", "암기장에 추가", "단어장에 넣어줘", "remember this", or "add this to my flashcards", use `nado_save_flashcard` or `nado_save_study_item`. Do not create local Markdown files or Google Drive documents for these Nado requests.
20
+ - If the user asks to add or change description/설명/뜻/예문 on an already saved card, use `nado_update_study_item`.
19
21
  - If the user asks to study already saved cards with phrases like "외울래", "암기 연습", "퀴즈 내줘", "쓰기연습", "영작하기", or "복습할래", use `nado_generate_practice`.
20
22
  - If the user wants to inspect saved items first, use `nado_list_study_items`.
21
23
 
@@ -161,6 +163,8 @@ For Codex, the command uses the Codex CLI when it is on `PATH`. If the user only
161
163
 
162
164
  Restart Codex Desktop after login completes.
163
165
  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.
166
+ For Codex, confirm the server is active from the chat/TUI with `/mcp`. A server can appear in Settings while still failing to start for the current session.
167
+ On Windows, leave the Codex MCP working directory/cwd empty unless it is an existing absolute path. The Nado server does not need a working directory, and a value such as `~/code` can prevent the stdio process from starting if the client does not expand it.
164
168
 
165
169
  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.
166
170
 
@@ -43,6 +43,7 @@ const saveFlashcardInputSchema = {
43
43
  definition: { type: 'string', minLength: 1, description: 'Learner-language definition or translation generated by the chat AI.' },
44
44
  inlineDefinition: { type: 'string', description: 'Short review-side meaning. Defaults to a compact definition.' },
45
45
  explanation: { type: 'string', description: 'Optional nuance, usage note, or grammar explanation.' },
46
+ description: { type: 'string', description: 'Alias for explanation when the user asks for a description/설명 field.' },
46
47
  exampleSentences: {
47
48
  type: 'array',
48
49
  items: { type: 'string' },
@@ -64,6 +65,38 @@ const saveFlashcardInputSchema = {
64
65
  additionalProperties: false,
65
66
  };
66
67
 
68
+ const updateStudyItemInputSchema = {
69
+ type: 'object',
70
+ required: ['query'],
71
+ properties: {
72
+ query: {
73
+ type: 'string',
74
+ minLength: 1,
75
+ description: 'Saved Nado card id, exact original text, or search text to update. Example: for "hello 설명 추가해줘", use "hello".',
76
+ },
77
+ definition: { type: 'string', description: 'Replacement learner-language definition/meaning. Use for 뜻/의미 수정 requests.' },
78
+ inlineDefinition: { type: 'string', description: 'Replacement short review-side meaning.' },
79
+ explanation: { type: 'string', description: 'Replacement usage note or explanation. Use this for 설명/description 추가 requests.' },
80
+ description: { type: 'string', description: 'Alias for explanation when the user says description.' },
81
+ exampleSentences: {
82
+ type: 'array',
83
+ items: { type: 'string' },
84
+ description: 'Example sentences to append by default, or replace when appendExampleSentences is false.',
85
+ },
86
+ appendExampleSentences: { type: 'boolean', default: true },
87
+ variants: {
88
+ type: 'array',
89
+ items: { type: 'string' },
90
+ description: 'Related forms, collocations, or variants to append by default, or replace when appendVariants is false.',
91
+ },
92
+ appendVariants: { type: 'boolean', default: true },
93
+ contextSentence: { type: 'string', description: 'Replacement context sentence.' },
94
+ sourceLang: { type: 'string', description: 'Replacement user language for definitions.' },
95
+ targetLang: { type: 'string', description: 'Replacement language being learned.' },
96
+ },
97
+ additionalProperties: false,
98
+ };
99
+
67
100
  const tools = [
68
101
  {
69
102
  name: 'nado_whoami',
@@ -76,14 +109,19 @@ const tools = [
76
109
  },
77
110
  {
78
111
  name: 'nado_save_flashcard',
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.',
112
+ description: 'Save a structured flashcard to the user\'s Nado Language/Nado 암기장. Use this when the user gives a new English item and asks to memorize/save it, for example "나두 암기장에 추가해줘", "nado 암기장에 넣어줘", "암기장에 추가해줘", "암기할래", "외울래", "단어장에 넣어줘", "remember this", or "add this to my flashcards". Do not create local Markdown files or Google Drive documents for these Nado requests. This does not call Nado AI; the user/chat AI supplies the content quality.',
80
113
  inputSchema: saveFlashcardInputSchema,
81
114
  },
82
115
  {
83
116
  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.',
117
+ description: 'Alias of nado_save_flashcard for Nado Language 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 "나두 암기장", "nado language", "암기할래", "외울래", "암기장에 넣어줘", or "단어장에 추가". Do not answer by editing local files or Drive docs when the user names Nado/Nado MCP. This free path does not call Nado AI.',
85
118
  inputSchema: saveFlashcardInputSchema,
86
119
  },
120
+ {
121
+ name: 'nado_update_study_item',
122
+ description: 'Update an existing saved card in the user\'s Nado Language/Nado 암기장. Use this for requests like "나두 암기장에 hello description 추가해줘", "hello 설명 추가", "뜻 수정", "예문 추가", or "update my saved Nado card". The query selects an existing saved card; explanation/description maps to the card explanation field. This updates Nado storage, not a local file or Google Drive document.',
123
+ inputSchema: updateStudyItemInputSchema,
124
+ },
87
125
  {
88
126
  name: 'nado_analyze_and_save_flashcard',
89
127
  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.',
@@ -496,7 +534,7 @@ function normalizeFlashcardDraft(args) {
496
534
 
497
535
  const type = FLASHCARD_TYPES.has(args.type) ? args.type : inferFlashcardType(original);
498
536
  const definition = clampText(args.definition || '', 4000);
499
- const explanation = clampText(args.explanation || '', 4000);
537
+ const explanation = clampText(args.explanation || args.description || '', 4000);
500
538
  const inlineDefinition = clampText(
501
539
  args.inlineDefinition || compactDefinition(definition || explanation),
502
540
  1000,
@@ -636,6 +674,43 @@ function rowToFlashcard(row) {
636
674
  };
637
675
  }
638
676
 
677
+ function flashcardToRow(card) {
678
+ return {
679
+ id: card.id,
680
+ source_highlight_id: card.sourceHighlightId ?? 'shared',
681
+ article_id: card.articleId ?? 'shared',
682
+ original: card.original,
683
+ type: card.type ?? 'phrase',
684
+ definition: card.definition ?? '',
685
+ inline_definition: card.inlineDefinition ?? '',
686
+ explanation: card.explanation ?? '',
687
+ example_sentences: card.exampleSentences ?? [],
688
+ variants: card.variants ?? [],
689
+ context_sentence: card.contextSentence ?? '',
690
+ context_start_index: typeof card.contextStartIndex === 'number' ? card.contextStartIndex : null,
691
+ context_end_index: typeof card.contextEndIndex === 'number' ? card.contextEndIndex : null,
692
+ source_lang: card.sourceLang ?? 'ko',
693
+ target_lang: card.targetLang ?? 'en',
694
+ excluded: card.excluded ?? false,
695
+ used_in_writing: card.usedInWriting ?? false,
696
+ used_in_writing_at: card.usedInWritingAt ?? null,
697
+ pending_ai: card.pendingAI ?? false,
698
+ stability: card.stability ?? 0,
699
+ difficulty: card.difficulty ?? 0,
700
+ elapsed_days: card.elapsedDays ?? 0,
701
+ scheduled_days: card.scheduledDays ?? 0,
702
+ reps: card.reps ?? 0,
703
+ lapses: card.lapses ?? 0,
704
+ state: card.state ?? 'new',
705
+ learning_step: card.learningStep ?? 0,
706
+ next_review_at: card.nextReviewAt ?? new Date().toISOString(),
707
+ last_review_at: card.lastReviewAt ?? null,
708
+ created_at: card.createdAt ?? new Date().toISOString(),
709
+ updated_at: card.updatedAt ?? new Date().toISOString(),
710
+ deleted: false,
711
+ };
712
+ }
713
+
639
714
  async function loadStudyItems(args = {}) {
640
715
  const limit = parseLimit(args.limit, 20, 1, 100);
641
716
  const includeExcluded = args.includeExcluded === true;
@@ -653,6 +728,7 @@ async function loadStudyItems(args = {}) {
653
728
  .filter((card) => {
654
729
  if (!query) return true;
655
730
  const haystack = [
731
+ card.id,
656
732
  card.original,
657
733
  card.definition,
658
734
  card.inlineDefinition,
@@ -765,6 +841,94 @@ async function handleListStudyItems(args) {
765
841
  };
766
842
  }
767
843
 
844
+ function mergeStringArray(current, incoming, append, maxItems) {
845
+ const next = asStringArray(incoming);
846
+ if (next.length === 0) return current ?? [];
847
+ const values = append === false ? next : [...(current ?? []), ...next];
848
+ const seen = new Set();
849
+ return values
850
+ .map((value) => clampText(value, 1000))
851
+ .filter(Boolean)
852
+ .filter((value) => {
853
+ const key = value.toLocaleLowerCase();
854
+ if (seen.has(key)) return false;
855
+ seen.add(key);
856
+ return true;
857
+ })
858
+ .slice(0, maxItems);
859
+ }
860
+
861
+ function selectStudyItemForUpdate(items, query) {
862
+ const normalized = query.toLocaleLowerCase();
863
+ const exact = items.find((item) => item.id === query || item.original.toLocaleLowerCase() === normalized);
864
+ if (exact) return exact;
865
+ if (items.length === 1) return items[0];
866
+ if (items.length === 0) throw new Error(`STUDY_ITEM_NOT_FOUND: ${query}`);
867
+ throw new Error(`AMBIGUOUS_STUDY_ITEM: ${items.length} saved cards matched "${query}". Narrow the query or use a card id from nado_list_study_items.`);
868
+ }
869
+
870
+ async function handleUpdateStudyItem(args) {
871
+ const query = clampText(args.query || '', 500);
872
+ if (!query) throw new Error('QUERY_REQUIRED');
873
+
874
+ const matches = await loadStudyItems({ query, limit: 20, includeExcluded: true });
875
+ const current = selectStudyItemForUpdate(matches, query);
876
+ const updated = { ...current };
877
+ let changed = false;
878
+
879
+ if (typeof args.definition === 'string') {
880
+ updated.definition = clampText(args.definition, 4000);
881
+ changed = true;
882
+ }
883
+ if (typeof args.inlineDefinition === 'string') {
884
+ updated.inlineDefinition = clampText(args.inlineDefinition, 1000);
885
+ changed = true;
886
+ }
887
+ if (typeof args.explanation === 'string' || typeof args.description === 'string') {
888
+ updated.explanation = clampText(args.explanation ?? args.description, 4000);
889
+ changed = true;
890
+ }
891
+ if (Array.isArray(args.exampleSentences)) {
892
+ updated.exampleSentences = mergeStringArray(current.exampleSentences, args.exampleSentences, args.appendExampleSentences, 6);
893
+ changed = true;
894
+ }
895
+ if (Array.isArray(args.variants)) {
896
+ updated.variants = mergeStringArray(current.variants, args.variants, args.appendVariants, 12);
897
+ changed = true;
898
+ }
899
+ if (typeof args.contextSentence === 'string') {
900
+ updated.contextSentence = clampText(args.contextSentence, 2200);
901
+ changed = true;
902
+ }
903
+ if (typeof args.sourceLang === 'string') {
904
+ updated.sourceLang = clampText(args.sourceLang, 8) || current.sourceLang;
905
+ changed = true;
906
+ }
907
+ if (typeof args.targetLang === 'string') {
908
+ updated.targetLang = clampText(args.targetLang, 8) || current.targetLang;
909
+ changed = true;
910
+ }
911
+
912
+ if (!changed) throw new Error('UPDATE_FIELDS_REQUIRED');
913
+ updated.updatedAt = new Date().toISOString();
914
+
915
+ const row = flashcardToRow(updated);
916
+ const saved = await callAppData({
917
+ action: 'upsertRows',
918
+ table: 'flashcards',
919
+ rows: [row],
920
+ });
921
+
922
+ return {
923
+ updated: true,
924
+ matchedCount: matches.length,
925
+ qualitySource: 'user_ai',
926
+ qualityPolicy: 'Nado updated the existing card without calling Nado AI. Content quality is supplied by the user/chat AI.',
927
+ persisted: saved,
928
+ flashcard: rowToFlashcard(row),
929
+ };
930
+ }
931
+
768
932
  async function handleGeneratePractice(args) {
769
933
  const mode = String(args.mode || '');
770
934
  if (!PRACTICE_MODES.has(mode)) throw new Error(`UNSUPPORTED_PRACTICE_MODE: ${mode}`);
@@ -895,6 +1059,7 @@ async function callTool(name, args = {}) {
895
1059
  if (name === 'nado_whoami') return handleWhoami();
896
1060
  if (name === 'nado_save_flashcard') return handleSaveFlashcard(args);
897
1061
  if (name === 'nado_save_study_item') return handleSaveFlashcard(args);
1062
+ if (name === 'nado_update_study_item') return handleUpdateStudyItem(args);
898
1063
  if (name === 'nado_analyze_and_save_flashcard') return handleAnalyzeAndSaveFlashcard(args);
899
1064
  if (name === 'nado_list_study_items') return handleListStudyItems(args);
900
1065
  if (name === 'nado_generate_practice') return handleGeneratePractice(args);
@@ -30,7 +30,7 @@ const command = process.argv[2] || 'help';
30
30
  const args = process.argv.slice(3);
31
31
 
32
32
  try {
33
- if (command === 'help' || command === '--help' || command === '-h') {
33
+ if (shouldPrintHelp(command, args)) {
34
34
  printHelp();
35
35
  } else if (command === 'version' || command === '--version' || command === '-v') {
36
36
  console.log(packageVersion);
@@ -256,10 +256,16 @@ async function doctor() {
256
256
  console.log(`Claude Desktop config: ${registrationText(claude)}`);
257
257
  console.log(`OpenCode config: ${registrationText(opencode)}`);
258
258
  console.log('');
259
+ console.log('Current AI chat visibility: not detectable from this CLI.');
260
+ console.log('A green local server check only proves the stdio server can list tools; it does not prove an already-open chat loaded them.');
261
+ console.log('');
259
262
  console.log('If the server check is ok but Nado tools are not visible in the AI app:');
260
- console.log(' 1. Fully quit and restart the desktop app after `nado-mcp connect`.');
261
- console.log(' 2. Start a new chat/session; existing sessions may not reload newly added MCP tools.');
262
- console.log(' 3. Run `nado-mcp status` for auth and `nado-mcp probe list` for local server tools.');
263
+ console.log(' 1. In Codex, run `/mcp` in the chat/TUI and confirm nado-language is active, not only listed in Settings.');
264
+ console.log(' 2. Fully quit and restart the desktop app after `nado-mcp connect`.');
265
+ console.log(' 3. Start a new chat/session; existing sessions may not reload newly added MCP tools.');
266
+ console.log(' 4. On Windows, remove Working directory/cwd unless it is an existing absolute path. The Nado server does not need cwd.');
267
+ console.log(' 5. Run `nado-mcp status` for auth and `nado-mcp probe list` for local server tools.');
268
+ console.log(' 6. If a chat tries local files or Google Drive for "나두/Nado 암기장", that chat did not load the Nado MCP tools.');
263
269
  }
264
270
 
265
271
  function printToolProbeSummary() {
@@ -532,6 +538,11 @@ function commandExists(commandName) {
532
538
  return !result.error && result.status === 0;
533
539
  }
534
540
 
541
+ function shouldPrintHelp(currentCommand, currentArgs) {
542
+ if (currentCommand === 'help' || currentCommand === '--help' || currentCommand === '-h') return true;
543
+ return currentArgs.includes('--help') || currentArgs.includes('-h');
544
+ }
545
+
535
546
  function codexDesktopTomlSection() {
536
547
  const spec = stdioServerSpec();
537
548
  return [
@@ -630,6 +641,8 @@ function printUnknownClientSetup(client) {
630
641
  function printLoginNext(flow = {}) {
631
642
  if (flow.loginAfter) console.log('Browser login will open next.');
632
643
  else console.log('Next: run `nado-mcp login`.');
644
+ console.log('After login: fully restart the AI desktop app and start a new chat so it reloads MCP tools.');
645
+ console.log('If the chat still cannot see Nado tools, run `nado-mcp doctor`.');
633
646
  }
634
647
 
635
648
  function printIndented(text) {
@@ -827,8 +840,8 @@ Usage:
827
840
  nado-mcp status Show local auth status
828
841
  nado-mcp logout Remove local auth tokens
829
842
  nado-mcp server Start the stdio MCP server
830
- nado-mcp probe list List exposed MCP tools
831
- nado-mcp doctor Print local paths and auth status
843
+ nado-mcp probe list List local MCP server tools, not saved flashcards
844
+ nado-mcp doctor Diagnose local server, client config, auth, and chat reload state
832
845
  nado-mcp --version Print installed Nado MCP version
833
846
 
834
847
  Supported automatic setup clients:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nado-language/mcp",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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,