smilenexus-mcp 1.0.0 → 1.0.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 +27 -0
- package/cli.mjs +217 -41
- 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,194 @@ 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
|
-
|
|
12
|
-
|
|
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 ||
|
|
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
|
-
|
|
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:
|
|
37
|
-
args: [
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
+
}
|
|
66
121
|
|
|
67
|
-
|
|
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');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Proxy Logic (stdio <-> HTTP) ──────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
let buf = '';
|
|
68
179
|
let pending = 0;
|
|
69
180
|
let stdinDone = false;
|
|
70
|
-
process.stdin.setEncoding(
|
|
181
|
+
process.stdin.setEncoding('utf8');
|
|
71
182
|
|
|
72
|
-
process.stdin.on(
|
|
183
|
+
process.stdin.on('data', (chunk) => {
|
|
73
184
|
buf += chunk;
|
|
74
185
|
let idx;
|
|
75
|
-
while ((idx = buf.indexOf(
|
|
186
|
+
while ((idx = buf.indexOf('\n')) !== -1) {
|
|
76
187
|
const line = buf.slice(0, idx).trim();
|
|
77
188
|
buf = buf.slice(idx + 1);
|
|
78
189
|
if (line) handle(line);
|
|
79
190
|
}
|
|
80
191
|
});
|
|
81
192
|
|
|
82
|
-
process.stdin.on(
|
|
193
|
+
process.stdin.on('end', () => {
|
|
83
194
|
stdinDone = true;
|
|
84
195
|
if (pending === 0) process.exit(0);
|
|
85
196
|
});
|
|
@@ -92,25 +203,90 @@ async function handle(line) {
|
|
|
92
203
|
return;
|
|
93
204
|
}
|
|
94
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
|
|
95
264
|
pending++;
|
|
96
265
|
try {
|
|
97
266
|
const res = await fetch(MCP_URL, {
|
|
98
|
-
method:
|
|
99
|
-
headers: {
|
|
267
|
+
method: 'POST',
|
|
268
|
+
headers: {
|
|
269
|
+
'content-type': 'application/json',
|
|
270
|
+
...(currentApiKey ? { authorization: `Bearer ${currentApiKey}` } : {}),
|
|
271
|
+
},
|
|
100
272
|
body: JSON.stringify(msg),
|
|
101
273
|
});
|
|
102
274
|
|
|
103
275
|
if (res.status !== 202) {
|
|
104
276
|
const json = await res.json();
|
|
105
|
-
|
|
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');
|
|
106
282
|
}
|
|
107
283
|
} catch (err) {
|
|
108
284
|
process.stdout.write(
|
|
109
285
|
JSON.stringify({
|
|
110
|
-
jsonrpc:
|
|
286
|
+
jsonrpc: '2.0',
|
|
111
287
|
id: msg?.id ?? null,
|
|
112
288
|
error: { code: -32603, message: String(err) },
|
|
113
|
-
}) +
|
|
289
|
+
}) + '\n',
|
|
114
290
|
);
|
|
115
291
|
} finally {
|
|
116
292
|
pending--;
|