@meldocio/mcp-stdio-proxy 1.0.18 ā 1.0.20
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 +22 -16
- package/bin/cli.js +8 -24
- package/bin/meldoc-mcp-proxy.js +69 -2
- package/lib/device-flow.js +316 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,6 +5,25 @@
|
|
|
5
5
|
|
|
6
6
|
This package allows you to connect Claude Desktop to your Meldoc account, so you can use all your documentation directly in Claude.
|
|
7
7
|
|
|
8
|
+
## š Quick Start - Install from Claude Marketplace
|
|
9
|
+
|
|
10
|
+
The easiest way to install Meldoc MCP is through the Claude Marketplace:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# Add the marketplace
|
|
14
|
+
claude plugin marketplace add meldoc-io/mcp-stdio-proxy
|
|
15
|
+
|
|
16
|
+
# Install the plugin
|
|
17
|
+
claude plugin install meldoc-mcp@meldoc-mcp
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
After installation:
|
|
21
|
+
|
|
22
|
+
1. Restart Claude Desktop (or your MCP client)
|
|
23
|
+
2. Run `npx @meldocio/mcp-stdio-proxy@latest auth login` to authenticate
|
|
24
|
+
|
|
25
|
+
Done! š Now you can ask Claude to work with your Meldoc documentation.
|
|
26
|
+
|
|
8
27
|
## What is this?
|
|
9
28
|
|
|
10
29
|
This is a bridge between Claude Desktop and Meldoc. After setup, Claude will be able to:
|
|
@@ -20,20 +39,7 @@ This is a bridge between Claude Desktop and Meldoc. After setup, Claude will be
|
|
|
20
39
|
|
|
21
40
|
### Via Claude Marketplace (Recommended) š
|
|
22
41
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
# Add the marketplace
|
|
27
|
-
claude plugin marketplace add meldoc/mcp-stdio-proxy
|
|
28
|
-
|
|
29
|
-
# Install the plugin
|
|
30
|
-
claude plugin install meldoc-mcp@meldoc
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
After installation:
|
|
34
|
-
|
|
35
|
-
1. Restart Claude Desktop (or your MCP client)
|
|
36
|
-
2. Run `npx @meldocio/mcp-stdio-proxy@latest auth login` to authenticate
|
|
42
|
+
See [Quick Start](#-quick-start---install-from-claude-marketplace) section above for the easiest installation method.
|
|
37
43
|
|
|
38
44
|
### Via NPM
|
|
39
45
|
|
|
@@ -481,7 +487,7 @@ If you're experiencing connection errors:
|
|
|
481
487
|
### Setup
|
|
482
488
|
|
|
483
489
|
```bash
|
|
484
|
-
git clone https://github.com/meldoc/mcp-stdio-proxy.git
|
|
490
|
+
git clone https://github.com/meldoc-io/mcp-stdio-proxy.git
|
|
485
491
|
cd mcp-stdio-proxy
|
|
486
492
|
npm install
|
|
487
493
|
```
|
|
@@ -545,7 +551,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
|
|
|
545
551
|
|
|
546
552
|
For issues, questions, or contributions, please visit:
|
|
547
553
|
|
|
548
|
-
- GitHub Issues: <https://github.com/meldoc/mcp-stdio-proxy/issues>
|
|
554
|
+
- GitHub Issues: <https://github.com/meldoc-io/mcp-stdio-proxy/issues>
|
|
549
555
|
- Documentation: <https://docs.meldoc.io>
|
|
550
556
|
|
|
551
557
|
## Related
|
package/bin/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { interactiveLogin } = require('../lib/device-flow');
|
|
4
4
|
const { readCredentials, deleteCredentials } = require('../lib/credentials');
|
|
5
5
|
const { getAuthStatus } = require('../lib/auth');
|
|
6
6
|
const { setWorkspaceAlias, getWorkspaceAlias } = require('../lib/config');
|
|
@@ -30,31 +30,15 @@ if (process.env.MELDOC_APP_URL) {
|
|
|
30
30
|
*/
|
|
31
31
|
async function handleAuthLogin() {
|
|
32
32
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
logger.info('Waiting for authentication...');
|
|
41
|
-
},
|
|
42
|
-
(status) => {
|
|
43
|
-
if (status === 'denied') {
|
|
44
|
-
logger.error('Login denied by user');
|
|
45
|
-
process.exit(1);
|
|
46
|
-
} else if (status === 'expired') {
|
|
47
|
-
logger.error('Authentication code expired');
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
API_URL,
|
|
52
|
-
APP_URL
|
|
53
|
-
);
|
|
54
|
-
logger.success('Login successful!');
|
|
33
|
+
await interactiveLogin({
|
|
34
|
+
autoOpen: true,
|
|
35
|
+
showQR: false,
|
|
36
|
+
timeout: 120000,
|
|
37
|
+
apiBaseUrl: API_URL,
|
|
38
|
+
appUrl: APP_URL
|
|
39
|
+
});
|
|
55
40
|
process.exit(0);
|
|
56
41
|
} catch (error) {
|
|
57
|
-
logger.error(`Login failed: ${error.message}`);
|
|
58
42
|
process.exit(1);
|
|
59
43
|
}
|
|
60
44
|
}
|
package/bin/meldoc-mcp-proxy.js
CHANGED
|
@@ -66,15 +66,21 @@ const LOG_LEVELS = {
|
|
|
66
66
|
// Import new auth and workspace modules
|
|
67
67
|
const { getAccessToken, getAuthStatus } = require('../lib/auth');
|
|
68
68
|
const { resolveWorkspaceAlias } = require('../lib/workspace');
|
|
69
|
-
const { getApiUrl } = require('../lib/constants');
|
|
69
|
+
const { getApiUrl, getAppUrl } = require('../lib/constants');
|
|
70
70
|
const { setWorkspaceAlias, getWorkspaceAlias } = require('../lib/config');
|
|
71
|
+
const { interactiveLogin, canOpenBrowser } = require('../lib/device-flow');
|
|
71
72
|
|
|
72
73
|
// Configuration
|
|
73
74
|
const apiUrl = getApiUrl();
|
|
75
|
+
const appUrl = getAppUrl();
|
|
74
76
|
const rpcEndpoint = `${apiUrl}/mcp/v1/rpc`;
|
|
75
77
|
const REQUEST_TIMEOUT = 25000; // 25 seconds (less than Claude Desktop's 30s timeout)
|
|
76
78
|
const LOG_LEVEL = getLogLevel(process.env.LOG_LEVEL || 'ERROR');
|
|
77
79
|
|
|
80
|
+
// Track if we've attempted auto-authentication
|
|
81
|
+
let autoAuthAttempted = false;
|
|
82
|
+
let autoAuthInProgress = false;
|
|
83
|
+
|
|
78
84
|
// Get log level from environment
|
|
79
85
|
function getLogLevel(level) {
|
|
80
86
|
const upper = (level || '').toUpperCase();
|
|
@@ -803,13 +809,74 @@ async function handleToolsCall(request) {
|
|
|
803
809
|
}
|
|
804
810
|
}
|
|
805
811
|
|
|
812
|
+
/**
|
|
813
|
+
* Attempt automatic authentication if conditions are met
|
|
814
|
+
* @returns {Promise<boolean>} True if authentication was attempted and succeeded
|
|
815
|
+
*/
|
|
816
|
+
async function attemptAutoAuth() {
|
|
817
|
+
// Only attempt once per session
|
|
818
|
+
if (autoAuthAttempted || autoAuthInProgress) {
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Only in interactive mode (TTY) and not in CI
|
|
823
|
+
if (!canOpenBrowser()) {
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Check if NO_AUTO_AUTH is set
|
|
828
|
+
if (process.env.NO_AUTO_AUTH === '1' || process.env.NO_AUTO_AUTH === 'true') {
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Check if token already exists
|
|
833
|
+
const tokenInfo = await getAccessToken();
|
|
834
|
+
if (tokenInfo) {
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
autoAuthAttempted = true;
|
|
839
|
+
autoAuthInProgress = true;
|
|
840
|
+
|
|
841
|
+
try {
|
|
842
|
+
log(LOG_LEVELS.INFO, 'š First time setup - authentication required');
|
|
843
|
+
process.stderr.write('\n');
|
|
844
|
+
|
|
845
|
+
await interactiveLogin({
|
|
846
|
+
autoOpen: true,
|
|
847
|
+
showQR: false,
|
|
848
|
+
timeout: 120000,
|
|
849
|
+
apiBaseUrl: apiUrl,
|
|
850
|
+
appUrl: appUrl
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
autoAuthInProgress = false;
|
|
854
|
+
return true;
|
|
855
|
+
} catch (error) {
|
|
856
|
+
autoAuthInProgress = false;
|
|
857
|
+
log(LOG_LEVELS.WARN, `Auto-authentication failed: ${error.message}`);
|
|
858
|
+
log(LOG_LEVELS.INFO, 'You can authenticate manually: npx @meldocio/mcp-stdio-proxy@latest auth login');
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
806
863
|
/**
|
|
807
864
|
* Process a single JSON-RPC request
|
|
808
865
|
* Forwards the request to the backend MCP API
|
|
809
866
|
*/
|
|
810
867
|
async function processSingleRequest(request) {
|
|
811
868
|
// Get access token with priority and auto-refresh
|
|
812
|
-
|
|
869
|
+
let tokenInfo = await getAccessToken();
|
|
870
|
+
|
|
871
|
+
// If no token and we haven't attempted auto-auth, try it
|
|
872
|
+
if (!tokenInfo && !autoAuthAttempted && !autoAuthInProgress) {
|
|
873
|
+
const authSucceeded = await attemptAutoAuth();
|
|
874
|
+
if (authSucceeded) {
|
|
875
|
+
// Retry getting token after successful auth
|
|
876
|
+
tokenInfo = await getAccessToken();
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
813
880
|
if (!tokenInfo) {
|
|
814
881
|
sendError(request.id, CUSTOM_ERROR_CODES.AUTH_REQUIRED,
|
|
815
882
|
'Meldoc token not found. Set MELDOC_ACCESS_TOKEN environment variable or run: npx @meldocio/mcp-stdio-proxy@latest auth login', {
|
package/lib/device-flow.js
CHANGED
|
@@ -3,6 +3,8 @@ const https = require('https');
|
|
|
3
3
|
const { writeCredentials } = require('./credentials');
|
|
4
4
|
const { getApiUrl, getAppUrl } = require('./constants');
|
|
5
5
|
const logger = require('./logger');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { exec } = require('child_process');
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Start device flow authentication
|
|
@@ -256,8 +258,321 @@ async function deviceFlowLogin(onCodeDisplay, onStatusChange = null, apiBaseUrl
|
|
|
256
258
|
throw new Error('Device code expired');
|
|
257
259
|
}
|
|
258
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Check if browser can be opened automatically
|
|
263
|
+
* @returns {boolean} True if browser can be opened
|
|
264
|
+
*/
|
|
265
|
+
function canOpenBrowser() {
|
|
266
|
+
return process.stdout.isTTY && !process.env.CI && !process.env.NO_BROWSER;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Open browser automatically
|
|
271
|
+
* @param {string} url - URL to open
|
|
272
|
+
* @returns {Promise<void>}
|
|
273
|
+
*/
|
|
274
|
+
async function openBrowser(url) {
|
|
275
|
+
try {
|
|
276
|
+
// Try using 'open' package first (cross-platform)
|
|
277
|
+
const open = require('open');
|
|
278
|
+
await open(url);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
// Fallback to platform-specific commands
|
|
281
|
+
try {
|
|
282
|
+
const platform = os.platform();
|
|
283
|
+
let command;
|
|
284
|
+
|
|
285
|
+
if (platform === 'darwin') {
|
|
286
|
+
command = `open "${url}"`;
|
|
287
|
+
} else if (platform === 'win32') {
|
|
288
|
+
command = `start "" "${url}"`;
|
|
289
|
+
} else {
|
|
290
|
+
command = `xdg-open "${url}"`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
exec(command, (err) => {
|
|
294
|
+
if (err) {
|
|
295
|
+
// Silent fail - browser opening is optional
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
} catch (fallbackError) {
|
|
299
|
+
// Silent fail - browser opening is optional
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Copy text to clipboard
|
|
306
|
+
* @param {string} text - Text to copy
|
|
307
|
+
* @returns {Promise<void>}
|
|
308
|
+
*/
|
|
309
|
+
async function copyToClipboard(text) {
|
|
310
|
+
try {
|
|
311
|
+
const clipboardy = require('clipboardy');
|
|
312
|
+
await clipboardy.write(text);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
// Silent fail - clipboard copy is optional
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Show QR code for mobile authentication
|
|
320
|
+
* @param {string} url - URL to encode in QR code
|
|
321
|
+
*/
|
|
322
|
+
function showQRCode(url) {
|
|
323
|
+
try {
|
|
324
|
+
const qrcode = require('qrcode-terminal');
|
|
325
|
+
qrcode.generate(url, { small: true });
|
|
326
|
+
} catch (error) {
|
|
327
|
+
// Silent fail - QR code is optional
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Sleep utility
|
|
333
|
+
* @param {number} ms - Milliseconds to sleep
|
|
334
|
+
* @returns {Promise<void>}
|
|
335
|
+
*/
|
|
336
|
+
function sleep(ms) {
|
|
337
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Poll for tokens with spinner and progress
|
|
342
|
+
* @param {string} deviceCode - Device code
|
|
343
|
+
* @param {number} interval - Polling interval in seconds
|
|
344
|
+
* @param {number} timeout - Timeout in milliseconds
|
|
345
|
+
* @param {string} apiBaseUrl - API base URL
|
|
346
|
+
* @returns {Promise<Object>} Credentials object
|
|
347
|
+
*/
|
|
348
|
+
async function pollForTokens({ deviceCode, interval, timeout, apiBaseUrl }) {
|
|
349
|
+
const startTime = Date.now();
|
|
350
|
+
const spinner = ['ā ', 'ā ', 'ā ¹', 'ā ø', 'ā ¼', 'ā “', 'ā ¦', 'ā §', 'ā ', 'ā '];
|
|
351
|
+
let spinnerIndex = 0;
|
|
352
|
+
let spinnerInterval = null;
|
|
353
|
+
|
|
354
|
+
// Start spinner animation if stderr is TTY
|
|
355
|
+
if (process.stderr.isTTY) {
|
|
356
|
+
spinnerInterval = setInterval(() => {
|
|
357
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
358
|
+
process.stderr.write(
|
|
359
|
+
`\r${spinner[spinnerIndex]} Waiting for authorization... (${elapsed}s)`
|
|
360
|
+
);
|
|
361
|
+
spinnerIndex = (spinnerIndex + 1) % spinner.length;
|
|
362
|
+
}, 100);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
while (true) {
|
|
367
|
+
// Check timeout
|
|
368
|
+
if (Date.now() - startTime > timeout) {
|
|
369
|
+
throw new Error('Authentication timeout. Please try again.');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
// Poll for status
|
|
374
|
+
const pollResponse = await pollDeviceFlow(deviceCode, apiBaseUrl);
|
|
375
|
+
|
|
376
|
+
if (pollResponse.status === 'approved') {
|
|
377
|
+
// Clear spinner
|
|
378
|
+
if (spinnerInterval) {
|
|
379
|
+
clearInterval(spinnerInterval);
|
|
380
|
+
process.stderr.write('\rā
Authorization confirmed! \n');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Validate that we have required fields
|
|
384
|
+
if (!pollResponse.accessToken) {
|
|
385
|
+
throw new Error(`Invalid poll response: missing accessToken. Response: ${JSON.stringify(pollResponse)}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return pollResponse;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (pollResponse.status === 'denied') {
|
|
392
|
+
throw new Error('Authorization denied by user');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (pollResponse.status === 'expired') {
|
|
396
|
+
throw new Error('Device code expired');
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// status === 'pending' - continue polling
|
|
400
|
+
await sleep(interval * 1000);
|
|
401
|
+
|
|
402
|
+
} catch (error) {
|
|
403
|
+
// If network error, retry
|
|
404
|
+
if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.message.includes('No response')) {
|
|
405
|
+
await sleep(interval * 1000);
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
} finally {
|
|
412
|
+
if (spinnerInterval) {
|
|
413
|
+
clearInterval(spinnerInterval);
|
|
414
|
+
process.stderr.write('\r \r');
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Interactive login with automatic browser opening and enhanced UX
|
|
421
|
+
* @param {Object} options - Login options
|
|
422
|
+
* @param {boolean} options.autoOpen - Automatically open browser (default: true)
|
|
423
|
+
* @param {boolean} options.showQR - Show QR code (default: false)
|
|
424
|
+
* @param {number} options.timeout - Timeout in milliseconds (default: 120000)
|
|
425
|
+
* @param {string} options.apiBaseUrl - API base URL
|
|
426
|
+
* @param {string} options.appUrl - App URL
|
|
427
|
+
* @returns {Promise<Object>} Credentials object
|
|
428
|
+
*/
|
|
429
|
+
async function interactiveLogin(options = {}) {
|
|
430
|
+
const {
|
|
431
|
+
autoOpen = true,
|
|
432
|
+
showQR = false,
|
|
433
|
+
timeout = 120000,
|
|
434
|
+
apiBaseUrl = null,
|
|
435
|
+
appUrl = null
|
|
436
|
+
} = options;
|
|
437
|
+
|
|
438
|
+
const url = apiBaseUrl || getApiUrl();
|
|
439
|
+
const frontendUrl = appUrl || getAppUrl();
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
// Step 1: Get device code from server
|
|
443
|
+
const startResponse = await startDeviceFlow(url);
|
|
444
|
+
const { deviceCode, userCode, verificationUrl, expiresIn, interval } = startResponse;
|
|
445
|
+
|
|
446
|
+
// Step 2: Build full URL
|
|
447
|
+
let displayUrl = verificationUrl;
|
|
448
|
+
|
|
449
|
+
// Check if verificationUrl already contains code in query parameter
|
|
450
|
+
let urlHasCode = false;
|
|
451
|
+
try {
|
|
452
|
+
const urlObj = new URL(verificationUrl);
|
|
453
|
+
if (urlObj.searchParams.has('code')) {
|
|
454
|
+
urlHasCode = true;
|
|
455
|
+
}
|
|
456
|
+
} catch (e) {
|
|
457
|
+
// URL parsing failed, continue
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (process.env.MELDOC_APP_URL || appUrl) {
|
|
461
|
+
try {
|
|
462
|
+
const urlObj = new URL(verificationUrl);
|
|
463
|
+
const appUrlObj = new URL(frontendUrl);
|
|
464
|
+
displayUrl = `${appUrlObj.origin}${urlObj.pathname}${urlObj.search}`;
|
|
465
|
+
} catch (e) {
|
|
466
|
+
displayUrl = verificationUrl;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// If URL doesn't have code, add it as path parameter
|
|
471
|
+
let fullUrl = displayUrl;
|
|
472
|
+
if (!urlHasCode) {
|
|
473
|
+
if (displayUrl.endsWith('/')) {
|
|
474
|
+
fullUrl = `${displayUrl}${userCode}`;
|
|
475
|
+
} else {
|
|
476
|
+
fullUrl = `${displayUrl}/${userCode}`;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Step 3: Display authentication UI
|
|
481
|
+
process.stderr.write('\n');
|
|
482
|
+
process.stderr.write('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
483
|
+
process.stderr.write('ā ā\n');
|
|
484
|
+
process.stderr.write('ā š Meldoc Authentication Required ā\n');
|
|
485
|
+
process.stderr.write('ā ā\n');
|
|
486
|
+
process.stderr.write('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n');
|
|
487
|
+
process.stderr.write('\n');
|
|
488
|
+
|
|
489
|
+
// Show QR code if requested
|
|
490
|
+
if (showQR) {
|
|
491
|
+
process.stderr.write('š± Scan QR code with your phone:\n\n');
|
|
492
|
+
showQRCode(fullUrl);
|
|
493
|
+
process.stderr.write('\n');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Show URL and code
|
|
497
|
+
process.stderr.write(`š Visit: ${fullUrl}\n`);
|
|
498
|
+
process.stderr.write(`š Enter this code: ${userCode}\n\n`);
|
|
499
|
+
process.stderr.write('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n\n');
|
|
500
|
+
|
|
501
|
+
// Step 4: Automatically open browser if enabled
|
|
502
|
+
const shouldOpenBrowser = autoOpen && canOpenBrowser();
|
|
503
|
+
if (shouldOpenBrowser) {
|
|
504
|
+
process.stderr.write('š Opening browser automatically...\n\n');
|
|
505
|
+
await openBrowser(fullUrl);
|
|
506
|
+
} else if (!canOpenBrowser()) {
|
|
507
|
+
process.stderr.write('ā ļø Please open the link manually in your browser\n\n');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Step 5: Copy code to clipboard
|
|
511
|
+
try {
|
|
512
|
+
await copyToClipboard(userCode);
|
|
513
|
+
process.stderr.write('ā
Code copied to clipboard!\n\n');
|
|
514
|
+
} catch (error) {
|
|
515
|
+
// Silent fail
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Step 6: Poll for authorization with spinner
|
|
519
|
+
process.stderr.write('ā³ Waiting for authorization...\n\n');
|
|
520
|
+
|
|
521
|
+
const pollResponse = await pollForTokens({
|
|
522
|
+
deviceCode,
|
|
523
|
+
interval,
|
|
524
|
+
timeout,
|
|
525
|
+
apiBaseUrl: url
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// Step 7: Save credentials
|
|
529
|
+
const credentials = {
|
|
530
|
+
type: 'user_session',
|
|
531
|
+
apiBaseUrl: url,
|
|
532
|
+
user: pollResponse.user,
|
|
533
|
+
tokens: {
|
|
534
|
+
accessToken: pollResponse.accessToken,
|
|
535
|
+
accessExpiresAt: pollResponse.expiresAt || new Date(Date.now() + 3600000).toISOString(),
|
|
536
|
+
refreshToken: pollResponse.refreshToken || null
|
|
537
|
+
},
|
|
538
|
+
updatedAt: new Date().toISOString()
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
writeCredentials(credentials);
|
|
542
|
+
|
|
543
|
+
process.stderr.write('\nā
Successfully authenticated!\n\n');
|
|
544
|
+
|
|
545
|
+
return credentials;
|
|
546
|
+
|
|
547
|
+
} catch (error) {
|
|
548
|
+
// Handle specific errors with helpful messages
|
|
549
|
+
if (error.code === 'ECONNREFUSED' || error.message.includes('No response')) {
|
|
550
|
+
process.stderr.write('\nā Cannot connect to Meldoc API\n');
|
|
551
|
+
process.stderr.write(' Please check your internet connection\n\n');
|
|
552
|
+
} else if (error.message.includes('timeout')) {
|
|
553
|
+
process.stderr.write('\nā±ļø Authentication timed out\n');
|
|
554
|
+
process.stderr.write(' Please try again or authenticate manually:\n');
|
|
555
|
+
process.stderr.write(' npx @meldocio/mcp-stdio-proxy@latest auth login\n\n');
|
|
556
|
+
} else if (error.message.includes('denied')) {
|
|
557
|
+
process.stderr.write('\nš« Authentication was denied\n');
|
|
558
|
+
process.stderr.write(' Please try again if this was a mistake\n\n');
|
|
559
|
+
} else {
|
|
560
|
+
process.stderr.write(`\nā Authentication failed: ${error.message}\n\n`);
|
|
561
|
+
process.stderr.write(' Manual authentication:\n');
|
|
562
|
+
process.stderr.write(' npx @meldocio/mcp-stdio-proxy@latest auth login\n\n');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
throw error;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
259
569
|
module.exports = {
|
|
260
570
|
startDeviceFlow,
|
|
261
571
|
pollDeviceFlow,
|
|
262
|
-
deviceFlowLogin
|
|
572
|
+
deviceFlowLogin,
|
|
573
|
+
interactiveLogin,
|
|
574
|
+
openBrowser,
|
|
575
|
+
copyToClipboard,
|
|
576
|
+
showQRCode,
|
|
577
|
+
canOpenBrowser
|
|
263
578
|
};
|