@nado-language/mcp 0.1.0

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.
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from 'node:child_process';
4
+ import { existsSync } from 'node:fs';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ const serverPath = firstExisting([
10
+ path.join(__dirname, 'nado-language-server.mjs'),
11
+ path.join(__dirname, '..', 'mcp', 'nado-language-server.mjs'),
12
+ ]);
13
+ const mode = process.argv[2] || 'list';
14
+ const args = process.argv.slice(3);
15
+
16
+ const child = spawn(process.execPath, [serverPath], {
17
+ cwd: path.join(__dirname, '..'),
18
+ env: process.env,
19
+ stdio: ['pipe', 'pipe', 'inherit'],
20
+ });
21
+
22
+ function firstExisting(candidates) {
23
+ return candidates.find((candidate) => existsSync(candidate)) || candidates[0];
24
+ }
25
+
26
+ let buffer = Buffer.alloc(0);
27
+ let nextId = 1;
28
+ const pending = new Map();
29
+
30
+ child.stdout.on('data', (chunk) => {
31
+ buffer = Buffer.concat([buffer, chunk]);
32
+ drain();
33
+ });
34
+
35
+ child.on('exit', (code) => {
36
+ if (code !== 0 && pending.size > 0) {
37
+ for (const { reject } of pending.values()) reject(new Error(`MCP server exited with code ${code}`));
38
+ pending.clear();
39
+ }
40
+ });
41
+
42
+ function send(method, params = {}) {
43
+ const id = nextId++;
44
+ const message = { jsonrpc: '2.0', id, method, params };
45
+ const payload = JSON.stringify(message);
46
+ child.stdin.write(`Content-Length: ${Buffer.byteLength(payload, 'utf8')}\r\n\r\n${payload}`);
47
+ return new Promise((resolve, reject) => pending.set(id, { resolve, reject }));
48
+ }
49
+
50
+ function notify(method, params = {}) {
51
+ const payload = JSON.stringify({ jsonrpc: '2.0', method, params });
52
+ child.stdin.write(`Content-Length: ${Buffer.byteLength(payload, 'utf8')}\r\n\r\n${payload}`);
53
+ }
54
+
55
+ function drain() {
56
+ while (true) {
57
+ const headerEnd = buffer.indexOf('\r\n\r\n');
58
+ if (headerEnd < 0) return;
59
+ const header = buffer.slice(0, headerEnd).toString('utf8');
60
+ const match = header.match(/content-length:\s*(\d+)/i);
61
+ if (!match) throw new Error(`Missing Content-Length in ${header}`);
62
+ const contentLength = Number(match[1]);
63
+ const frameEnd = headerEnd + 4 + contentLength;
64
+ if (buffer.length < frameEnd) return;
65
+ const payload = buffer.slice(headerEnd + 4, frameEnd).toString('utf8');
66
+ buffer = buffer.slice(frameEnd);
67
+
68
+ const message = JSON.parse(payload);
69
+ const request = pending.get(message.id);
70
+ if (!request) continue;
71
+ pending.delete(message.id);
72
+ if (message.error) request.reject(new Error(message.error.message));
73
+ else request.resolve(message.result);
74
+ }
75
+ }
76
+
77
+ try {
78
+ await send('initialize', {
79
+ protocolVersion: '2025-06-18',
80
+ capabilities: {},
81
+ clientInfo: { name: 'nado-mcp-probe', version: '0.1.0' },
82
+ });
83
+ notify('notifications/initialized');
84
+
85
+ if (mode === 'list') {
86
+ const result = await send('tools/list');
87
+ console.log(JSON.stringify(result, null, 2));
88
+ } else if (mode === 'whoami') {
89
+ const result = await send('tools/call', { name: 'nado_whoami', arguments: {} });
90
+ console.log(result.content?.[0]?.text || JSON.stringify(result, null, 2));
91
+ } else if (mode === 'save') {
92
+ const text = args.join(' ').trim() || `codex-mcp-test-${Date.now()}`;
93
+ const result = await send('tools/call', {
94
+ name: 'nado_save_flashcard',
95
+ arguments: {
96
+ original: text,
97
+ type: text.split(/\s+/).length > 1 ? 'phrase' : 'word',
98
+ definition: `Probe definition for ${text}`,
99
+ inlineDefinition: `Probe: ${text}`,
100
+ explanation: 'Saved from the local MCP probe without Nado AI.',
101
+ exampleSentences: [`Codex saved "${text}" through the Nado MCP server.`],
102
+ variants: [],
103
+ contextSentence: `Codex saved "${text}" through the Nado MCP server.`,
104
+ articleTitle: 'Codex MCP Probe',
105
+ articleId: 'codex-mcp-probe',
106
+ sourceHighlightId: `codex-mcp-probe-${Date.now()}`,
107
+ },
108
+ });
109
+ console.log(result.content?.[0]?.text || JSON.stringify(result, null, 2));
110
+ } else if (mode === 'save-nado-ai') {
111
+ const text = args.join(' ').trim() || `codex-mcp-pro-${Date.now()}`;
112
+ const result = await send('tools/call', {
113
+ name: 'nado_analyze_and_save_flashcard',
114
+ arguments: {
115
+ original: text,
116
+ contextSentence: `Codex asked Nado AI to analyze "${text}" through the MCP server.`,
117
+ articleTitle: 'Codex MCP Pro Probe',
118
+ articleId: 'codex-mcp-pro-probe',
119
+ sourceHighlightId: `codex-mcp-pro-${Date.now()}`,
120
+ },
121
+ });
122
+ console.log(result.content?.[0]?.text || JSON.stringify(result, null, 2));
123
+ } else if (mode === 'list-items') {
124
+ const query = args.join(' ').trim();
125
+ const result = await send('tools/call', {
126
+ name: 'nado_list_study_items',
127
+ arguments: { query, limit: 10 },
128
+ });
129
+ console.log(result.content?.[0]?.text || JSON.stringify(result, null, 2));
130
+ } else if (mode === 'practice') {
131
+ const practiceMode = args[0] || 'sentence_completion';
132
+ const query = args.slice(1).join(' ').trim();
133
+ const result = await send('tools/call', {
134
+ name: 'nado_generate_practice',
135
+ arguments: { mode: practiceMode, query, limit: 3 },
136
+ });
137
+ console.log(result.content?.[0]?.text || JSON.stringify(result, null, 2));
138
+ } else if (mode === 'verify-admin') {
139
+ const text = args.join(' ').trim() || `codex-mcp-admin-${Date.now()}`;
140
+ const verification = await verifyAdminFlow(text);
141
+ console.log(JSON.stringify(verification, null, 2));
142
+ } else {
143
+ throw new Error(`Unknown probe mode: ${mode}`);
144
+ }
145
+ } catch (error) {
146
+ console.error(`Nado MCP probe failed: ${error instanceof Error ? error.message : String(error)}`);
147
+ process.exitCode = 1;
148
+ } finally {
149
+ child.stdin.end();
150
+ }
151
+
152
+ async function callTool(name, toolArgs = {}) {
153
+ const result = await send('tools/call', { name, arguments: toolArgs });
154
+ const text = result.content?.[0]?.text;
155
+ const parsed = text ? JSON.parse(text) : result.structuredContent;
156
+ if (result.isError || parsed?.error) {
157
+ throw new Error(parsed?.error || `Tool failed: ${name}`);
158
+ }
159
+ return parsed;
160
+ }
161
+
162
+ async function verifyAdminFlow(text) {
163
+ const whoami = await callTool('nado_whoami');
164
+ const identity = [
165
+ whoami.user?.email,
166
+ whoami.profile?.display_name,
167
+ whoami.profile?.provider,
168
+ whoami.profile?.id,
169
+ ].filter(Boolean).join(' ').toLowerCase();
170
+
171
+ if (!identity.includes('jintonyc')) {
172
+ throw new Error(`EXPECTED_ADMIN_JINTONYC: authenticated account did not look like jintonyc (${identity || 'empty identity'})`);
173
+ }
174
+
175
+ const saved = await callTool('nado_save_flashcard', {
176
+ original: text,
177
+ type: text.split(/\s+/).length > 1 ? 'phrase' : 'word',
178
+ definition: `Admin verification definition for ${text}`,
179
+ inlineDefinition: `Admin verification: ${text}`,
180
+ explanation: 'Saved from the MCP admin verification without Nado AI cost.',
181
+ exampleSentences: [`Codex verification saved "${text}" through the Nado MCP server.`],
182
+ variants: [],
183
+ contextSentence: `Codex verification saved "${text}" through the Nado MCP server.`,
184
+ articleTitle: 'Codex MCP Admin Verification',
185
+ articleId: 'codex-mcp-admin-verification',
186
+ sourceHighlightId: `codex-mcp-admin-${Date.now()}`,
187
+ });
188
+
189
+ const listed = await callTool('nado_list_study_items', { query: text, limit: 10 });
190
+ const savedCard = listed.items?.find((item) => item.id === saved.flashcard?.id || item.original === text);
191
+ if (!savedCard) {
192
+ throw new Error(`SAVED_CARD_NOT_FOUND: ${text}`);
193
+ }
194
+
195
+ const practice = await callTool('nado_generate_practice', {
196
+ mode: 'sentence_completion',
197
+ query: text,
198
+ limit: 3,
199
+ });
200
+
201
+ const materialIds = new Set((practice.materials || []).map((item) => item.id));
202
+ const exerciseIds = new Set((practice.exercises || []).flatMap((exercise) => exercise.sourceCardIds || []));
203
+ const citesOnlySavedMaterials = [...exerciseIds].every((id) => materialIds.has(id));
204
+
205
+ if (practice.onlySavedMaterials !== true || !citesOnlySavedMaterials || practice.exercises?.length < 1) {
206
+ throw new Error('PRACTICE_POLICY_FAILED: generated practice did not prove saved-material-only usage.');
207
+ }
208
+
209
+ return {
210
+ ok: true,
211
+ user: whoami.user,
212
+ profile: whoami.profile,
213
+ savedFlashcardId: saved.flashcard.id,
214
+ savedOriginal: saved.flashcard.original,
215
+ listedCount: listed.count,
216
+ practiceMode: practice.mode,
217
+ practiceMaterialCount: practice.materialCount,
218
+ practiceExerciseCount: practice.exercises.length,
219
+ onlySavedMaterials: practice.onlySavedMaterials,
220
+ citedSourceCardIds: [...exerciseIds],
221
+ };
222
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@nado-language/mcp",
3
+ "version": "0.1.0",
4
+ "description": "Nado Language MCP server for saving AI-generated English flashcards and practicing saved materials.",
5
+ "type": "module",
6
+ "private": false,
7
+ "license": "UNLICENSED",
8
+ "bin": {
9
+ "nado-mcp": "dist/nado-mcp-cli.mjs",
10
+ "nado-mcp-server": "dist/nado-language-server.mjs",
11
+ "nado-mcp-auth": "dist/nado-mcp-auth.mjs"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "README.md"
16
+ ],
17
+ "engines": {
18
+ "node": ">=20"
19
+ },
20
+ "scripts": {
21
+ "prepack": "node ../../scripts/build-nado-mcp-package.mjs",
22
+ "test": "node dist/nado-mcp-cli.mjs doctor"
23
+ },
24
+ "keywords": [
25
+ "mcp",
26
+ "nado",
27
+ "language-learning",
28
+ "flashcards"
29
+ ]
30
+ }