smilenexus-mcp 1.0.0 → 1.0.2

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.
Files changed (3) hide show
  1. package/README.md +27 -0
  2. package/cli.mjs +213 -41
  3. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # smilenexus-mcp
2
+
3
+ MCP (Model Context Protocol) connector for smileNexus.
4
+
5
+ ## Usage
6
+
7
+ This package is intended to be run via `npx` and handles authentication and tunneling for the smileNexus MCP server.
8
+
9
+ ```bash
10
+ npx -y smilenexus-mcp login
11
+ ```
12
+
13
+ ## Development & Publishing
14
+
15
+ To publish a new version of this package to npm:
16
+
17
+ 1. **Bump the version number** (This will update `package.json`):
18
+ ```bash
19
+ cd smilenexus-mcp
20
+ npm version patch # Use minor or major if appropriate
21
+ ```
22
+
23
+ 2. **Publish to npm**:
24
+ ```bash
25
+ npm publish
26
+ ```
27
+ *(Note: You must be logged in to npm via `npm login` with the appropriate account before publishing)*
package/cli.mjs CHANGED
@@ -3,83 +3,190 @@ import fs from 'fs';
3
3
  import path from 'path';
4
4
  import os from 'os';
5
5
 
6
+ // ── Credentials helpers ────────────────────────────────────────────────────────
7
+
8
+ const CREDS_PATH = path.join(os.homedir(), '.config', 'smilenexus', 'credentials.json');
9
+
10
+ function readCredentials() {
11
+ try {
12
+ if (!fs.existsSync(CREDS_PATH)) return null;
13
+ return JSON.parse(fs.readFileSync(CREDS_PATH, 'utf8'));
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ function writeCredentials(data) {
20
+ const dir = path.dirname(CREDS_PATH);
21
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
22
+ fs.writeFileSync(CREDS_PATH, JSON.stringify(data, null, 2), { mode: 0o600 });
23
+ }
24
+
25
+ // ── Config ────────────────────────────────────────────────────────────────────
26
+
27
+ const MCP_URL = process.env.SMILENEXUS_URL ?? 'https://smile-nexus-dev.kantapon-r.workers.dev/mcp/sse';
28
+
29
+ // mutable — updated after successful login tool call during active MCP session
30
+ let currentApiKey = process.env.SMILENEXUS_API_KEY || readCredentials()?.api_key || null;
31
+
32
+ // ── Login tool definition (injected client-side into tools/list) ──────────────
33
+
34
+ const LOGIN_TOOL_DEF = {
35
+ name: 'login',
36
+ description: 'เชื่อมต่อ smileNexus ด้วย Microsoft account — รัน tool นี้ก่อนใช้งาน tool อื่น หรือเมื่อต้องการ rotate key',
37
+ inputSchema: { type: 'object', properties: {}, required: [] },
38
+ };
39
+
40
+ // ── Command dispatch ──────────────────────────────────────────────────────────
41
+
6
42
  const args = process.argv.slice(2);
7
43
  const command = args[0];
8
44
 
9
45
  if (command === 'install' || command === 'install-codex' || command === 'install-agy') {
10
46
  const customUrl = args[1];
11
- console.log(`\n========================================`);
12
- console.log(` smileNexus MCP Auto-Installer`);
13
- console.log(`========================================\n`);
14
-
47
+ process.stderr.write(`\n========================================\n smileNexus MCP Auto-Installer\n========================================\n\n`);
48
+
15
49
  if (command === 'install') {
16
- const defaultUrl = customUrl || "https://smile-nexus.kantapon-r.workers.dev/mcp/sse";
50
+ const defaultUrl = customUrl || 'https://smile-nexus.kantapon-r.workers.dev/mcp/sse';
17
51
  let configPath;
18
-
19
52
  if (process.platform === 'darwin') {
20
53
  configPath = path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
21
54
  } else if (process.platform === 'win32') {
22
55
  configPath = path.join(os.homedir(), 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
23
56
  } else {
24
- console.log('Unsupported OS for auto-install. Please configure manually using the JSON provided in the UI.');
57
+ process.stderr.write('Unsupported OS for auto-install. Please configure manually.\n');
25
58
  process.exit(0);
26
59
  }
27
-
60
+
61
+ let installOk = false;
28
62
  try {
29
63
  let config = { mcpServers: {} };
30
- if (fs.existsSync(configPath)) {
31
- config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
32
- }
33
-
64
+ if (fs.existsSync(configPath)) config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
34
65
  config.mcpServers = config.mcpServers || {};
35
66
  config.mcpServers.smileNexus = {
36
- command: "npx",
37
- args: ["-y", "smilenexus-mcp"],
38
- env: {
39
- "SMILENEXUS_URL": defaultUrl
40
- }
67
+ command: 'npx',
68
+ args: ['-y', 'smilenexus-mcp'],
69
+ env: { SMILENEXUS_URL: defaultUrl },
41
70
  };
42
-
43
71
  const dir = path.dirname(configPath);
44
72
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
45
-
46
73
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
47
- console.log(`✅ Successfully added smileNexus to Claude Desktop!`);
48
- console.log(`Config updated at: ${configPath}`);
49
- console.log(`Please restart Claude Desktop for the changes to take effect.`);
74
+ process.stderr.write(`✅ Successfully added smileNexus to Claude Desktop!\nConfig updated at: ${configPath}\n`);
75
+ installOk = true;
50
76
  } catch (err) {
51
- console.error(`❌ Failed to update Claude config: ${err.message}`);
52
- console.log(`Please configure manually using the JSON provided in the UI.`);
77
+ process.stderr.write(`❌ Failed to update Claude config: ${err.message}\n`);
78
+ }
79
+
80
+ // Option B: auto-login immediately after install (mandatory — login is required to use smileNexus)
81
+ if (installOk) {
82
+ process.stderr.write(`\n🔐 กำลัง login เพื่อเปิดใช้งาน...\n`);
83
+ try {
84
+ await runLogin(new URL(defaultUrl).origin);
85
+ process.stderr.write(`\nRestart Claude Desktop แล้วใช้งาน smileNexus ได้เลย!\n`);
86
+ } catch {
87
+ process.stderr.write(`\nLogin ล้มเหลว กรุณารัน \`npx smilenexus-mcp login\` ก่อนใช้งาน\n`);
88
+ }
53
89
  }
54
90
  } else {
55
- console.log(`Auto-install for ${command} is coming soon!`);
56
- console.log(`Please configure manually using the instructions in the UI.`);
91
+ process.stderr.write(`Auto-install for ${command} is coming soon!\nPlease configure manually.\n`);
57
92
  }
58
-
59
93
  process.exit(0);
60
94
  }
61
95
 
62
- // ---------------------------------------------------------
63
- // Proxy Logic (stdio <-> HTTP)
64
- // ---------------------------------------------------------
65
- const MCP_URL = process.env.SMILENEXUS_URL ?? "https://smile-nexus-dev.kantapon-r.workers.dev/mcp/sse";
96
+ if (command === 'login') {
97
+ try {
98
+ await runLogin();
99
+ process.exit(0);
100
+ } catch {
101
+ process.exit(1);
102
+ }
103
+ }
104
+
105
+ // ── Login flow ────────────────────────────────────────────────────────────────
106
+ // Throws on failure instead of calling process.exit — callers decide how to handle.
107
+
108
+ async function runLogin(baseUrl) {
109
+ if (!baseUrl) baseUrl = new URL(MCP_URL).origin;
110
+
111
+ // 1. Create session
112
+ let sessionData;
113
+ try {
114
+ const res = await fetch(`${baseUrl}/auth/mcp/session`, { method: 'POST' });
115
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
116
+ sessionData = await res.json();
117
+ } catch (err) {
118
+ process.stderr.write(`❌ ไม่สามารถสร้าง login session ได้: ${err.message}\n`);
119
+ throw err;
120
+ }
121
+
122
+ const { session_code, login_url } = sessionData;
123
+
124
+ // 2. Open browser
125
+ process.stderr.write(`\n🔐 เปิด URL นี้ใน browser:\n\n ${login_url}\n\nรอการ login...\n`);
126
+ try {
127
+ const { spawn } = await import('child_process');
128
+ const opener = process.platform === 'darwin' ? 'open'
129
+ : process.platform === 'win32' ? 'start'
130
+ : 'xdg-open';
131
+ spawn(opener, [login_url], { detached: true, stdio: 'ignore' }).unref();
132
+ } catch {}
133
+
134
+ // 3. Poll every 2s, max 10 minutes
135
+ const MAX_POLLS = 300;
136
+ for (let i = 0; i < MAX_POLLS; i++) {
137
+ await new Promise(r => setTimeout(r, 2000));
138
+
139
+ let pollData;
140
+ let retries = 0;
141
+ while (true) {
142
+ try {
143
+ const res = await fetch(`${baseUrl}/auth/mcp/poll?code=${session_code}`);
144
+ pollData = await res.json();
145
+ break;
146
+ } catch {
147
+ retries++;
148
+ if (retries >= 3) {
149
+ process.stderr.write('❌ Network error. กรุณาตรวจสอบการเชื่อมต่อ\n');
150
+ throw new Error('Network error after 3 retries');
151
+ }
152
+ await new Promise(r => setTimeout(r, 2000));
153
+ }
154
+ }
155
+
156
+ if (pollData.status === 'done') {
157
+ writeCredentials({ api_key: pollData.api_key, created_at: new Date().toISOString() });
158
+ process.stderr.write(`\n✅ Login สำเร็จ! smileNexus พร้อมใช้งานแล้ว\n`);
159
+ return;
160
+ }
161
+ if (pollData.status === 'expired') {
162
+ process.stderr.write('❌ Session หมดอายุ กรุณาลองใหม่\n');
163
+ throw new Error('Session expired');
164
+ }
165
+ // status === 'pending' — continue polling
166
+ }
167
+
168
+ process.stderr.write('❌ หมดเวลา กรุณาลอง `npx smilenexus-mcp login` ใหม่\n');
169
+ throw new Error('Login timeout');
170
+ }
171
+
172
+ // ── Proxy Logic (stdio <-> HTTP) ──────────────────────────────────────────────
66
173
 
67
- let buf = "";
174
+ let buf = '';
68
175
  let pending = 0;
69
176
  let stdinDone = false;
70
- process.stdin.setEncoding("utf8");
177
+ process.stdin.setEncoding('utf8');
71
178
 
72
- process.stdin.on("data", (chunk) => {
179
+ process.stdin.on('data', (chunk) => {
73
180
  buf += chunk;
74
181
  let idx;
75
- while ((idx = buf.indexOf("\n")) !== -1) {
182
+ while ((idx = buf.indexOf('\n')) !== -1) {
76
183
  const line = buf.slice(0, idx).trim();
77
184
  buf = buf.slice(idx + 1);
78
185
  if (line) handle(line);
79
186
  }
80
187
  });
81
188
 
82
- process.stdin.on("end", () => {
189
+ process.stdin.on('end', () => {
83
190
  stdinDone = true;
84
191
  if (pending === 0) process.exit(0);
85
192
  });
@@ -92,25 +199,90 @@ async function handle(line) {
92
199
  return;
93
200
  }
94
201
 
202
+ // Option A: intercept tools/list — inject login tool
203
+ if (msg.method === 'tools/list') {
204
+ if (!currentApiKey) {
205
+ // Not authenticated — return only the login tool so the AI knows what to call
206
+ process.stdout.write(JSON.stringify({
207
+ jsonrpc: '2.0',
208
+ id: msg.id,
209
+ result: { tools: [LOGIN_TOOL_DEF] },
210
+ }) + '\n');
211
+ return;
212
+ }
213
+ // Authenticated — proxy and append login tool to the result
214
+ pending++;
215
+ try {
216
+ const res = await fetch(MCP_URL, {
217
+ method: 'POST',
218
+ headers: { 'content-type': 'application/json', authorization: `Bearer ${currentApiKey}` },
219
+ body: JSON.stringify(msg),
220
+ });
221
+ if (res.status !== 202) {
222
+ const json = await res.json();
223
+ if (json.result?.tools) json.result.tools.push(LOGIN_TOOL_DEF);
224
+ process.stdout.write(JSON.stringify(json) + '\n');
225
+ }
226
+ } catch (err) {
227
+ process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: msg?.id ?? null, error: { code: -32603, message: String(err) } }) + '\n');
228
+ } finally {
229
+ pending--;
230
+ if (stdinDone && pending === 0) process.exit(0);
231
+ }
232
+ return;
233
+ }
234
+
235
+ // Option A: intercept tools/call for the login tool — handle locally
236
+ if (msg.method === 'tools/call' && msg.params?.name === 'login') {
237
+ pending++;
238
+ try {
239
+ await runLogin();
240
+ currentApiKey = readCredentials()?.api_key ?? null;
241
+ process.stdout.write(JSON.stringify({
242
+ jsonrpc: '2.0',
243
+ id: msg.id,
244
+ result: { content: [{ type: 'text', text: '✅ Login สำเร็จ! smileNexus พร้อมใช้งานแล้ว ลองใช้ tool อื่นได้เลย' }] },
245
+ }) + '\n');
246
+ } catch (err) {
247
+ process.stdout.write(JSON.stringify({
248
+ jsonrpc: '2.0',
249
+ id: msg.id,
250
+ result: { content: [{ type: 'text', text: `❌ Login ล้มเหลว: ${err.message} — กรุณาลองใหม่` }], isError: true },
251
+ }) + '\n');
252
+ } finally {
253
+ pending--;
254
+ if (stdinDone && pending === 0) process.exit(0);
255
+ }
256
+ return;
257
+ }
258
+
259
+ // Normal proxy
95
260
  pending++;
96
261
  try {
97
262
  const res = await fetch(MCP_URL, {
98
- method: "POST",
99
- headers: { "content-type": "application/json" },
263
+ method: 'POST',
264
+ headers: {
265
+ 'content-type': 'application/json',
266
+ ...(currentApiKey ? { authorization: `Bearer ${currentApiKey}` } : {}),
267
+ },
100
268
  body: JSON.stringify(msg),
101
269
  });
102
270
 
103
271
  if (res.status !== 202) {
104
272
  const json = await res.json();
105
- process.stdout.write(JSON.stringify(json) + "\n");
273
+ // Transform auth error into actionable hint for the AI
274
+ if (!currentApiKey && json.error?.code === -32600) {
275
+ json.error.message = 'smileNexus: ยังไม่ได้ authenticate — กรุณาเรียกใช้ tool "login" เพื่อเชื่อมต่อก่อน';
276
+ }
277
+ process.stdout.write(JSON.stringify(json) + '\n');
106
278
  }
107
279
  } catch (err) {
108
280
  process.stdout.write(
109
281
  JSON.stringify({
110
- jsonrpc: "2.0",
282
+ jsonrpc: '2.0',
111
283
  id: msg?.id ?? null,
112
284
  error: { code: -32603, message: String(err) },
113
- }) + "\n",
285
+ }) + '\n',
114
286
  );
115
287
  } finally {
116
288
  pending--;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smilenexus-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "MCP connector for smileNexus",
5
5
  "type": "module",
6
6
  "bin": "./cli.mjs",