smilenexus-mcp 1.0.4 → 1.0.5

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 +13 -0
  2. package/cli.mjs +340 -249
  3. package/package.json +7 -7
package/README.md CHANGED
@@ -6,10 +6,23 @@ MCP (Model Context Protocol) connector for smileNexus.
6
6
 
7
7
  This package is intended to be run via `npx` and handles authentication and tunneling for the smileNexus MCP server.
8
8
 
9
+ **Install & login (dev):**
10
+ ```bash
11
+ npx smilenexus-mcp install
12
+ ```
13
+
14
+ **Install with a custom URL (e.g. production):**
15
+ ```bash
16
+ npx smilenexus-mcp install https://smile-nexus.kantapon-r.workers.dev/mcp/sse
17
+ ```
18
+
19
+ **Login only (no config update):**
9
20
  ```bash
10
21
  npx -y smilenexus-mcp login
11
22
  ```
12
23
 
24
+ > **Note:** The default install URL points to the dev environment (`smile-nexus-dev`). To use production, pass the URL explicitly and ensure `https://smile-nexus.kantapon-r.workers.dev/auth/callback` is registered in the Azure app registration.
25
+
13
26
  ## Development & Publishing
14
27
 
15
28
  To publish a new version of this package to npm:
package/cli.mjs CHANGED
@@ -1,40 +1,49 @@
1
1
  #!/usr/bin/env node
2
- import fs from 'fs';
3
- import path from 'path';
4
- import os from 'os';
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
5
 
6
6
  // ── Credentials helpers ────────────────────────────────────────────────────────
7
7
 
8
- const CREDS_PATH = path.join(os.homedir(), '.config', 'smilenexus', 'credentials.json');
8
+ const CREDS_PATH = path.join(
9
+ os.homedir(),
10
+ ".config",
11
+ "smilenexus",
12
+ "credentials.json",
13
+ );
9
14
 
10
15
  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
- }
16
+ try {
17
+ if (!fs.existsSync(CREDS_PATH)) return null;
18
+ return JSON.parse(fs.readFileSync(CREDS_PATH, "utf8"));
19
+ } catch {
20
+ return null;
21
+ }
17
22
  }
18
23
 
19
24
  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 });
25
+ const dir = path.dirname(CREDS_PATH);
26
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
27
+ fs.writeFileSync(CREDS_PATH, JSON.stringify(data, null, 2), { mode: 0o600 });
23
28
  }
24
29
 
25
30
  // ── Config ────────────────────────────────────────────────────────────────────
26
31
 
27
- const MCP_URL = process.env.SMILENEXUS_URL ?? 'https://smile-nexus-dev.kantapon-r.workers.dev/mcp/sse';
32
+ const MCP_URL =
33
+ process.env.SMILENEXUS_URL ??
34
+ "https://smile-nexus-dev.kantapon-r.workers.dev/mcp/sse";
28
35
 
29
36
  // mutable — updated after successful login tool call during active MCP session
30
- let currentApiKey = process.env.SMILENEXUS_API_KEY || readCredentials()?.api_key || null;
37
+ let currentApiKey =
38
+ process.env.SMILENEXUS_API_KEY || readCredentials()?.api_key || null;
31
39
 
32
40
  // ── Login tool definition (injected client-side into tools/list) ──────────────
33
41
 
34
42
  const LOGIN_TOOL_DEF = {
35
- name: 'login',
36
- description: 'เชื่อมต่อ smileNexus ด้วย Microsoft account — รัน tool นี้ก่อนใช้งาน tool อื่น หรือเมื่อต้องการ rotate key',
37
- inputSchema: { type: 'object', properties: {}, required: [] },
43
+ name: "login",
44
+ description:
45
+ "เชื่อมต่อ smileNexus ด้วย Microsoft account รัน tool นี้ก่อนใช้งาน tool อื่น หรือเมื่อต้องการ rotate key",
46
+ inputSchema: { type: "object", properties: {}, required: [] },
38
47
  };
39
48
 
40
49
  // ── Command dispatch ──────────────────────────────────────────────────────────
@@ -42,254 +51,336 @@ const LOGIN_TOOL_DEF = {
42
51
  const args = process.argv.slice(2);
43
52
  const command = args[0];
44
53
 
45
- if (command === 'install' || command === 'install-codex' || command === 'install-agy') {
46
- const customUrl = args[1];
47
- process.stderr.write(`\n========================================\n smileNexus MCP Auto-Installer\n========================================\n\n`);
48
-
49
- if (command === 'install') {
50
- const defaultUrl = customUrl || 'https://smile-nexus.kantapon-r.workers.dev/mcp/sse';
51
- let configPath;
52
- if (process.platform === 'darwin') {
53
- configPath = path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
54
- } else if (process.platform === 'win32') {
55
- configPath = path.join(os.homedir(), 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json');
56
- } else {
57
- process.stderr.write('Unsupported OS for auto-install. Please configure manually.\n');
58
- process.exit(0);
59
- }
60
-
61
- let installOk = false;
62
- try {
63
- let config = { mcpServers: {} };
64
- if (fs.existsSync(configPath)) config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
65
- config.mcpServers = config.mcpServers || {};
66
- config.mcpServers.smileNexus = {
67
- command: 'npx',
68
- args: ['-y', 'smilenexus-mcp'],
69
- env: { SMILENEXUS_URL: defaultUrl },
70
- };
71
- const dir = path.dirname(configPath);
72
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
73
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
74
- process.stderr.write(`✅ Successfully added smileNexus to Claude Desktop!\nConfig updated at: ${configPath}\n`);
75
- installOk = true;
76
- } catch (err) {
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
- }
89
- }
90
- } else {
91
- process.stderr.write(`Auto-install for ${command} is coming soon!\nPlease configure manually.\n`);
92
- }
93
- process.exit(0);
54
+ if (
55
+ command === "install" ||
56
+ command === "install-codex" ||
57
+ command === "install-agy"
58
+ ) {
59
+ const customUrl = args[1];
60
+ process.stderr.write(
61
+ `\n========================================\n smileNexus MCP Auto-Installer\n========================================\n\n`,
62
+ );
63
+
64
+ if (command === "install") {
65
+ const defaultUrl =
66
+ customUrl || "https://smile-nexus-dev.kantapon-r.workers.dev/mcp/sse";
67
+ let configPath;
68
+ if (process.platform === "darwin") {
69
+ configPath = path.join(
70
+ os.homedir(),
71
+ "Library",
72
+ "Application Support",
73
+ "Claude",
74
+ "claude_desktop_config.json",
75
+ );
76
+ } else if (process.platform === "win32") {
77
+ configPath = path.join(
78
+ os.homedir(),
79
+ "AppData",
80
+ "Roaming",
81
+ "Claude",
82
+ "claude_desktop_config.json",
83
+ );
84
+ } else {
85
+ process.stderr.write(
86
+ "Unsupported OS for auto-install. Please configure manually.\n",
87
+ );
88
+ process.exit(0);
89
+ }
90
+
91
+ let installOk = false;
92
+ try {
93
+ let config = { mcpServers: {} };
94
+ if (fs.existsSync(configPath))
95
+ config = JSON.parse(fs.readFileSync(configPath, "utf8"));
96
+ config.mcpServers = config.mcpServers || {};
97
+ config.mcpServers.smileNexus = {
98
+ command: "npx",
99
+ args: ["-y", "smilenexus-mcp"],
100
+ env: { SMILENEXUS_URL: defaultUrl },
101
+ };
102
+ const dir = path.dirname(configPath);
103
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
104
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
105
+ process.stderr.write(
106
+ `✅ Successfully added smileNexus to Claude Desktop!\nConfig updated at: ${configPath}\n`,
107
+ );
108
+ installOk = true;
109
+ } catch (err) {
110
+ process.stderr.write(
111
+ `❌ Failed to update Claude config: ${err.message}\n`,
112
+ );
113
+ }
114
+
115
+ // Option B: auto-login immediately after install (mandatory — login is required to use smileNexus)
116
+ if (installOk) {
117
+ process.stderr.write(`\n🔐 กำลัง login เพื่อเปิดใช้งาน...\n`);
118
+ try {
119
+ await runLogin(new URL(defaultUrl).origin);
120
+ process.stderr.write(
121
+ `\nRestart Claude Desktop แล้วใช้งาน smileNexus ได้เลย!\n`,
122
+ );
123
+ } catch {
124
+ process.stderr.write(
125
+ `\nLogin ล้มเหลว กรุณารัน \`npx smilenexus-mcp login\` ก่อนใช้งาน\n`,
126
+ );
127
+ }
128
+ }
129
+ } else {
130
+ process.stderr.write(
131
+ `Auto-install for ${command} is coming soon!\nPlease configure manually.\n`,
132
+ );
133
+ }
134
+ process.exit(0);
94
135
  }
95
136
 
96
- if (command === 'login') {
97
- try {
98
- await runLogin();
99
- process.exit(0);
100
- } catch {
101
- process.exit(1);
102
- }
137
+ if (command === "login") {
138
+ try {
139
+ await runLogin();
140
+ process.exit(0);
141
+ } catch {
142
+ process.exit(1);
143
+ }
103
144
  }
104
145
 
105
146
  // ── Login flow ────────────────────────────────────────────────────────────────
106
147
  // Throws on failure instead of calling process.exit — callers decide how to handle.
107
148
 
108
149
  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`);
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
- const spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
137
- let spinIdx = 0;
138
-
139
- for (let i = 0; i < MAX_POLLS; i++) {
140
- process.stderr.write(`\r${spinner[spinIdx++ % spinner.length]} รอการ login... `);
141
- await new Promise(r => setTimeout(r, 2000));
142
-
143
- let pollData;
144
- let retries = 0;
145
- while (true) {
146
- try {
147
- const res = await fetch(`${baseUrl}/auth/mcp/poll?code=${session_code}`);
148
- pollData = await res.json();
149
- break;
150
- } catch {
151
- retries++;
152
- if (retries >= 3) {
153
- process.stderr.write('❌ Network error. กรุณาตรวจสอบการเชื่อมต่อ\n');
154
- throw new Error('Network error after 3 retries');
155
- }
156
- await new Promise(r => setTimeout(r, 2000));
157
- }
158
- }
159
-
160
- if (pollData.status === 'done') {
161
- writeCredentials({ api_key: pollData.api_key, created_at: new Date().toISOString() });
162
- process.stderr.write(`\r\x1b[K✅ Login สำเร็จ! smileNexus พร้อมใช้งานแล้ว\n`);
163
- return;
164
- }
165
- if (pollData.status === 'expired') {
166
- process.stderr.write('\r\x1b[K❌ Session หมดอายุ กรุณาลองใหม่\n');
167
- throw new Error('Session expired');
168
- }
169
- // status === 'pending' — continue polling
170
- }
171
-
172
- process.stderr.write('❌ หมดเวลา กรุณาลอง `npx smilenexus-mcp login` ใหม่\n');
173
- throw new Error('Login timeout');
150
+ if (!baseUrl) baseUrl = new URL(MCP_URL).origin;
151
+
152
+ // 1. Create session
153
+ let sessionData;
154
+ try {
155
+ const res = await fetch(`${baseUrl}/auth/mcp/session`, { method: "POST" });
156
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
157
+ sessionData = await res.json();
158
+ } catch (err) {
159
+ process.stderr.write(`❌ ไม่สามารถสร้าง login session ได้: ${err.message}\n`);
160
+ throw err;
161
+ }
162
+
163
+ const { session_code, login_url } = sessionData;
164
+
165
+ // 2. Open browser
166
+ process.stderr.write(`\n🔐 เปิด URL นี้ใน browser:\n\n ${login_url}\n\n`);
167
+ try {
168
+ const { spawn } = await import("child_process");
169
+ let proc;
170
+ if (process.platform === "win32") {
171
+ proc = spawn("cmd", ["/c", "start", "", login_url], {
172
+ detached: true,
173
+ stdio: "ignore",
174
+ });
175
+ } else {
176
+ const opener = process.platform === "darwin" ? "open" : "xdg-open";
177
+ proc = spawn(opener, [login_url], { detached: true, stdio: "ignore" });
178
+ }
179
+ proc.on("error", () => {
180
+ process.stderr.write(
181
+ `⚠️ ไม่สามารถเปิด browser อัตโนมัติ กรุณาเปิด URL ด้านบนด้วยตนเอง\n`,
182
+ );
183
+ });
184
+ proc.unref();
185
+ } catch {}
186
+
187
+ // 3. Poll every 2s, max 10 minutes
188
+ const MAX_POLLS = 300;
189
+ const spinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
190
+ let spinIdx = 0;
191
+
192
+ for (let i = 0; i < MAX_POLLS; i++) {
193
+ process.stderr.write(
194
+ `\r${spinner[spinIdx++ % spinner.length]} รอการ login... `,
195
+ );
196
+ await new Promise((r) => setTimeout(r, 2000));
197
+
198
+ let pollData;
199
+ let retries = 0;
200
+ while (true) {
201
+ try {
202
+ const res = await fetch(
203
+ `${baseUrl}/auth/mcp/poll?code=${session_code}`,
204
+ );
205
+ pollData = await res.json();
206
+ break;
207
+ } catch {
208
+ retries++;
209
+ if (retries >= 3) {
210
+ process.stderr.write("❌ Network error. กรุณาตรวจสอบการเชื่อมต่อ\n");
211
+ throw new Error("Network error after 3 retries");
212
+ }
213
+ await new Promise((r) => setTimeout(r, 2000));
214
+ }
215
+ }
216
+
217
+ if (pollData.status === "done") {
218
+ writeCredentials({
219
+ api_key: pollData.api_key,
220
+ created_at: new Date().toISOString(),
221
+ });
222
+ process.stderr.write(`\r\x1b[K✅ Login สำเร็จ! smileNexus พร้อมใช้งานแล้ว\n`);
223
+ return;
224
+ }
225
+ if (pollData.status === "expired") {
226
+ process.stderr.write("\r\x1b[K❌ Session หมดอายุ กรุณาลองใหม่\n");
227
+ throw new Error("Session expired");
228
+ }
229
+ // status === 'pending' — continue polling
230
+ }
231
+
232
+ process.stderr.write("❌ หมดเวลา กรุณาลอง `npx smilenexus-mcp login` ใหม่\n");
233
+ throw new Error("Login timeout");
174
234
  }
175
235
 
176
236
  // ── Proxy Logic (stdio <-> HTTP) ──────────────────────────────────────────────
177
237
 
178
- let buf = '';
238
+ let buf = "";
179
239
  let pending = 0;
180
240
  let stdinDone = false;
181
- process.stdin.setEncoding('utf8');
182
-
183
- process.stdin.on('data', (chunk) => {
184
- buf += chunk;
185
- let idx;
186
- while ((idx = buf.indexOf('\n')) !== -1) {
187
- const line = buf.slice(0, idx).trim();
188
- buf = buf.slice(idx + 1);
189
- if (line) handle(line);
190
- }
241
+ process.stdin.setEncoding("utf8");
242
+
243
+ process.stdin.on("data", (chunk) => {
244
+ buf += chunk;
245
+ while (buf.includes("\n")) {
246
+ const idx = buf.indexOf("\n");
247
+ const line = buf.slice(0, idx).trim();
248
+ buf = buf.slice(idx + 1);
249
+ if (line) handle(line);
250
+ }
191
251
  });
192
252
 
193
- process.stdin.on('end', () => {
194
- stdinDone = true;
195
- if (pending === 0) process.exit(0);
253
+ process.stdin.on("end", () => {
254
+ stdinDone = true;
255
+ if (pending === 0) process.exit(0);
196
256
  });
197
257
 
198
258
  async function handle(line) {
199
- let msg;
200
- try {
201
- msg = JSON.parse(line);
202
- } catch {
203
- return;
204
- }
205
-
206
- // Option A: intercept tools/list — inject login tool
207
- if (msg.method === 'tools/list') {
208
- if (!currentApiKey) {
209
- // Not authenticated — return only the login tool so the AI knows what to call
210
- process.stdout.write(JSON.stringify({
211
- jsonrpc: '2.0',
212
- id: msg.id,
213
- result: { tools: [LOGIN_TOOL_DEF] },
214
- }) + '\n');
215
- return;
216
- }
217
- // Authenticated — proxy and append login tool to the result
218
- pending++;
219
- try {
220
- const res = await fetch(MCP_URL, {
221
- method: 'POST',
222
- headers: { 'content-type': 'application/json', authorization: `Bearer ${currentApiKey}` },
223
- body: JSON.stringify(msg),
224
- });
225
- if (res.status !== 202) {
226
- const json = await res.json();
227
- if (json.result?.tools) json.result.tools.push(LOGIN_TOOL_DEF);
228
- process.stdout.write(JSON.stringify(json) + '\n');
229
- }
230
- } catch (err) {
231
- process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: msg?.id ?? null, error: { code: -32603, message: String(err) } }) + '\n');
232
- } finally {
233
- pending--;
234
- if (stdinDone && pending === 0) process.exit(0);
235
- }
236
- return;
237
- }
238
-
239
- // Option A: intercept tools/call for the login tool — handle locally
240
- if (msg.method === 'tools/call' && msg.params?.name === 'login') {
241
- pending++;
242
- try {
243
- await runLogin();
244
- currentApiKey = readCredentials()?.api_key ?? null;
245
- process.stdout.write(JSON.stringify({
246
- jsonrpc: '2.0',
247
- id: msg.id,
248
- result: { content: [{ type: 'text', text: '✅ Login สำเร็จ! smileNexus พร้อมใช้งานแล้ว ลองใช้ tool อื่นได้เลย' }] },
249
- }) + '\n');
250
- } catch (err) {
251
- process.stdout.write(JSON.stringify({
252
- jsonrpc: '2.0',
253
- id: msg.id,
254
- result: { content: [{ type: 'text', text: `❌ Login ล้มเหลว: ${err.message} — กรุณาลองใหม่` }], isError: true },
255
- }) + '\n');
256
- } finally {
257
- pending--;
258
- if (stdinDone && pending === 0) process.exit(0);
259
- }
260
- return;
261
- }
262
-
263
- // Normal proxy
264
- pending++;
265
- try {
266
- const res = await fetch(MCP_URL, {
267
- method: 'POST',
268
- headers: {
269
- 'content-type': 'application/json',
270
- ...(currentApiKey ? { authorization: `Bearer ${currentApiKey}` } : {}),
271
- },
272
- body: JSON.stringify(msg),
273
- });
274
-
275
- if (res.status !== 202) {
276
- const json = await res.json();
277
- // Transform auth error into actionable hint for the AI
278
- if (!currentApiKey && json.error?.code === -32600) {
279
- json.error.message = 'smileNexus: ยังไม่ได้ authenticateกรุณาเรียกใช้ tool "login" เพื่อเชื่อมต่อก่อน';
280
- }
281
- process.stdout.write(JSON.stringify(json) + '\n');
282
- }
283
- } catch (err) {
284
- process.stdout.write(
285
- JSON.stringify({
286
- jsonrpc: '2.0',
287
- id: msg?.id ?? null,
288
- error: { code: -32603, message: String(err) },
289
- }) + '\n',
290
- );
291
- } finally {
292
- pending--;
293
- if (stdinDone && pending === 0) process.exit(0);
294
- }
259
+ let msg;
260
+ try {
261
+ msg = JSON.parse(line);
262
+ } catch {
263
+ return;
264
+ }
265
+
266
+ // Option A: intercept tools/list — inject login tool
267
+ if (msg.method === "tools/list") {
268
+ if (!currentApiKey) {
269
+ // Not authenticated — return only the login tool so the AI knows what to call
270
+ process.stdout.write(
271
+ JSON.stringify({
272
+ jsonrpc: "2.0",
273
+ id: msg.id,
274
+ result: { tools: [LOGIN_TOOL_DEF] },
275
+ }) + "\n",
276
+ );
277
+ return;
278
+ }
279
+ // Authenticated — proxy and append login tool to the result
280
+ pending++;
281
+ try {
282
+ const res = await fetch(MCP_URL, {
283
+ method: "POST",
284
+ headers: {
285
+ "content-type": "application/json",
286
+ authorization: `Bearer ${currentApiKey}`,
287
+ },
288
+ body: JSON.stringify(msg),
289
+ });
290
+ if (res.status !== 202) {
291
+ const json = await res.json();
292
+ if (json.result?.tools) json.result.tools.push(LOGIN_TOOL_DEF);
293
+ process.stdout.write(JSON.stringify(json) + "\n");
294
+ }
295
+ } catch (err) {
296
+ process.stdout.write(
297
+ JSON.stringify({
298
+ jsonrpc: "2.0",
299
+ id: msg?.id ?? null,
300
+ error: { code: -32603, message: String(err) },
301
+ }) + "\n",
302
+ );
303
+ } finally {
304
+ pending--;
305
+ if (stdinDone && pending === 0) process.exit(0);
306
+ }
307
+ return;
308
+ }
309
+
310
+ // Option A: intercept tools/call for the login tool — handle locally
311
+ if (msg.method === "tools/call" && msg.params?.name === "login") {
312
+ pending++;
313
+ try {
314
+ await runLogin();
315
+ currentApiKey = readCredentials()?.api_key ?? null;
316
+ process.stdout.write(
317
+ JSON.stringify({
318
+ jsonrpc: "2.0",
319
+ id: msg.id,
320
+ result: {
321
+ content: [
322
+ {
323
+ type: "text",
324
+ text: "✅ Login สำเร็จ! smileNexus พร้อมใช้งานแล้ว ลองใช้ tool อื่นได้เลย",
325
+ },
326
+ ],
327
+ },
328
+ }) + "\n",
329
+ );
330
+ } catch (err) {
331
+ process.stdout.write(
332
+ JSON.stringify({
333
+ jsonrpc: "2.0",
334
+ id: msg.id,
335
+ result: {
336
+ content: [
337
+ {
338
+ type: "text",
339
+ text: `❌ Login ล้มเหลว: ${err.message}กรุณาลองใหม่`,
340
+ },
341
+ ],
342
+ isError: true,
343
+ },
344
+ }) + "\n",
345
+ );
346
+ } finally {
347
+ pending--;
348
+ if (stdinDone && pending === 0) process.exit(0);
349
+ }
350
+ return;
351
+ }
352
+
353
+ // Normal proxy
354
+ pending++;
355
+ try {
356
+ const res = await fetch(MCP_URL, {
357
+ method: "POST",
358
+ headers: {
359
+ "content-type": "application/json",
360
+ ...(currentApiKey ? { authorization: `Bearer ${currentApiKey}` } : {}),
361
+ },
362
+ body: JSON.stringify(msg),
363
+ });
364
+
365
+ if (res.status !== 202) {
366
+ const json = await res.json();
367
+ // Transform auth error into actionable hint for the AI
368
+ if (!currentApiKey && json.error?.code === -32600) {
369
+ json.error.message =
370
+ 'smileNexus: ยังไม่ได้ authenticate — กรุณาเรียกใช้ tool "login" เพื่อเชื่อมต่อก่อน';
371
+ }
372
+ process.stdout.write(JSON.stringify(json) + "\n");
373
+ }
374
+ } catch (err) {
375
+ process.stdout.write(
376
+ JSON.stringify({
377
+ jsonrpc: "2.0",
378
+ id: msg?.id ?? null,
379
+ error: { code: -32603, message: String(err) },
380
+ }) + "\n",
381
+ );
382
+ } finally {
383
+ pending--;
384
+ if (stdinDone && pending === 0) process.exit(0);
385
+ }
295
386
  }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
- "name": "smilenexus-mcp",
3
- "version": "1.0.4",
4
- "description": "MCP connector for smileNexus",
5
- "type": "module",
6
- "bin": "./cli.mjs",
7
- "author": "Smile Fokus",
8
- "license": "MIT"
2
+ "name": "smilenexus-mcp",
3
+ "version": "1.0.5",
4
+ "description": "MCP connector for smileNexus",
5
+ "type": "module",
6
+ "bin": "./cli.mjs",
7
+ "author": "Smile Fokus",
8
+ "license": "MIT"
9
9
  }